Compare commits

...

1315 Commits

Author SHA1 Message Date
Arik Fraimovich
4a52ccd4fa Gitter integration for CircleCI. 2014-09-14 18:23:02 +03:00
Arik Fraimovich
a0c81f8a31 Merge pull request #281 from EverythingMe/fix_stuck_jobs
Several fixes to reduce cases of stuck jobs
2014-09-11 07:50:35 +03:00
Arik Fraimovich
ce13b79bdc Use correct logging level 2014-09-11 07:47:30 +03:00
Arik Fraimovich
c580db277d Add cleanup_tasks job.
Enumerates all locks and removes those of non existing jobs. Useful
for case the worker is being cold restarted, and jobs are finished
properly.
2014-09-11 07:42:36 +03:00
Arik Fraimovich
5e944e9a8f If found lock is for a ready job, ignore it.
ready - revoked, finished or failed.
2014-09-11 07:41:43 +03:00
Arik Fraimovich
4b94cf706a Set default locks expiry time to 12 hours 2014-09-11 07:41:23 +03:00
Arik Fraimovich
364c51456d Set expiry time to locks, just in case for some reason they get stuck. 2014-09-11 07:40:20 +03:00
Arik Fraimovich
1274d36abc Merge pull request #280 from EverythingMe/fix_stuck_jobs
Fix #261: cancelling jobs sends them to limbo
2014-09-06 18:12:03 +03:00
Arik Fraimovich
f6bd562dd2 Remove cleanup_tasks, as it's not stable 2014-09-06 18:09:04 +03:00
Arik Fraimovich
065d2bc2c6 Schedule removal of dead tasks 2014-09-06 14:18:35 +03:00
Arik Fraimovich
653ed1c57a Add cleanup task to remove locks of dead jobs 2014-09-06 14:18:15 +03:00
Arik Fraimovich
7dc1176628 Fix #261: cancelling jobs sends them to limbo 2014-09-06 13:56:36 +03:00
Arik Fraimovich
365b8a8c93 Merge pull request #279 from EverythingMe/json-results
API - query results in JSON format. fixes #278
2014-09-03 12:07:36 +03:00
Arik Fraimovich
6e1e0a9967 Merge QueryResultAPI with CSVQueryResultAPI 2014-09-03 11:55:17 +03:00
Amir Nissim
170640a63f API - query results in JSON format. fixes #278 2014-09-02 17:52:04 +03:00
Arik Fraimovich
5e970b73d5 Merge pull request #270 from olgakogan/master
added handling for querying strings with non standard characters
2014-08-25 12:00:02 +03:00
olgakogan
a4643472a5 added handling for querying strings with non standard characters 2014-08-24 19:08:10 +03:00
Arik Fraimovich
7aa01f2bd2 Comment out filters url sync tests. 2014-08-20 09:07:08 +03:00
Arik Fraimovich
cb4b0e0296 Merge pull request #269 from EverythingMe/257-chart-editor
Disable filters url syncing
2014-08-20 08:59:22 +03:00
Arik Fraimovich
2c05e921c4 Disable filters url syncing 2014-08-20 08:58:56 +03:00
Arik Fraimovich
c4877f254e Merge pull request #268 from EverythingMe/257-chart-editor
[#257] chart editor: global series type
2014-08-19 19:51:57 +03:00
Arik Fraimovich
9fc59de35f remove throttling of redrawData 2014-08-19 18:37:32 +03:00
Amir Nissim
eb50f3fc94 [#257] chart editor: use globalSeriesType when creating new series 2014-08-19 14:44:53 +03:00
Arik Fraimovich
12fe59827f Merge pull request #267 from EverythingMe/257-chart-editor
[#257] chart editor: global series type
2014-08-19 14:04:44 +03:00
Arik Fraimovich
d32caff31d Merge pull request #266 from EverythingMe/265-db-reloads
disable reloadOnSearch for /dashboard. fixes #265
2014-08-19 13:17:17 +03:00
Amir Nissim
ba540ff380 [#257] chart editor: global series type 2014-08-19 13:14:24 +03:00
Amir Nissim
2112faab02 disable reloadOnSearch for /dashboard. fixes #265 2014-08-19 12:01:23 +03:00
Arik Fraimovich
34c6be398a Merge pull request #264 from EverythingMe/fix_data_error
Treat all psycopg2.DatabaseError the same.
2014-08-19 09:53:38 +03:00
Arik Fraimovich
3f9c2a5592 Treat all psycopg2.DatabaseError the same.
Sometimes division by zero are reported as OperationalError rather than
DataError.
2014-08-19 09:47:31 +03:00
Arik Fraimovich
8076b7f0b7 Gruntfile.js: add login.html back to minified files. 2014-08-12 13:34:39 +03:00
Arik Fraimovich
8940d66b0b Merge pull request #253 from EverythingMe/146-filter-sync
rd_ui: sync filters with location.search [closes #146]
2014-08-07 14:28:06 +03:00
Amir Nissim
948e2247e4 rd_ui: sync filters with location.search [closes #146] 2014-08-07 14:11:43 +03:00
Arik Fraimovich
eba2ba1918 Merge pull request #260 from EverythingMe/fix_queue_name
Fix: dashboard filters broken after #252
2014-08-07 08:20:01 +03:00
Arik Fraimovich
59d5ba9273 Use promises to create dashboard filters. 2014-08-06 23:39:30 +03:00
Arik Fraimovich
4aba24a976 Add promise support to QueryResult. 2014-08-06 23:39:09 +03:00
Arik Fraimovich
762c331ddf Merge pull request #259 from EverythingMe/fix_queue_name
Fix events import code
2014-08-06 17:58:28 +03:00
Amir Nissim
9592610f8b update .gitignore 2014-08-06 16:19:09 +03:00
Arik Fraimovich
8b7399ddc9 Fix events import code 2014-08-06 09:31:19 +03:00
Arik Fraimovich
f6221da9dc Merge pull request #256 from EverythingMe/fix_queue_name
Fix: series options not showing up when first running the query.
2014-08-05 12:42:43 +03:00
Arik Fraimovich
10c84d2cd0 Fix: series options not showing up when first running the query. 2014-08-05 12:39:35 +03:00
Arik Fraimovich
60d784d7bc Cleanup Query.prototype.getQueryResult and make sure it caches result by id. 2014-08-05 12:38:53 +03:00
Arik Fraimovich
b28e4be8d7 Sort data sources by id. 2014-08-05 12:30:51 +03:00
Arik Fraimovich
e74b36996f Merge pull request #255 from EverythingMe/fix_queue_name
Fix: use correct queue name for scheduled queries
2014-08-04 22:40:16 +03:00
Arik Fraimovich
4c28d11259 Fix: use correct queue name for scheduled queries 2014-08-04 22:31:13 +03:00
Arik Fraimovich
b1e1a32f37 Merge pull request #252 from EverythingMe/perf
perf: HTTP caching headers for /api/query_results [fixes #228]
2014-08-04 16:55:39 +03:00
Amir Nissim
a12b43265d perf: HTTP caching headers for /api/query_results [fixes #228] 2014-08-04 16:50:56 +03:00
Arik Fraimovich
c2d621ae0f Merge pull request #247 from EverythingMe/245-refresh-btn
[#245] Add refresh button to query view page
2014-08-03 14:51:06 +03:00
Amir Nissim
d93e07061b [#245] Add refresh button to query view page 2014-08-03 13:14:17 +03:00
Arik Fraimovich
cb59973b9a Merge pull request #251 from EverythingMe/tests
setup Karma unit tests
2014-08-03 11:27:38 +03:00
Amir Nissim
72e41a94e4 update ci config 2014-08-03 11:15:02 +03:00
Amir Nissim
9013497fc7 rd_ui: fix failing unit test 2014-08-03 11:15:00 +03:00
Amir Nissim
a74ae32122 testing infra: basic QueryViewCtrl tests 2014-07-31 16:11:37 +03:00
Amir Nissim
9cfae349da testing infra: updated Karma and Grunt 2014-07-30 14:28:00 +03:00
Arik Fraimovich
a16718917b Merge pull request #248 from EverythingMe/243-db-requests
#243 dashboards api will not return query results by default
2014-07-29 16:14:08 +03:00
Amir Nissim
e2e365d9ff Query.to_dict never with results 2014-07-29 11:11:40 +03:00
Amir Nissim
5310498d0f [#241] fix textbox widget layout 2014-07-28 17:17:20 +03:00
Amir Nissim
bb1d2f8805 [#243] dashboards api will not return query results by default 2014-07-28 16:52:19 +03:00
Amir Nissim
0d5f001d38 fix migration add_text_to_widgets 2014-07-28 16:27:23 +03:00
Amir Nissim
236f7f9c04 fix add_global_filters_to_dashboard migration script 2014-07-28 12:15:08 +03:00
Amir Nissim
74bf8e5239 ignore celery files 2014-07-28 12:08:59 +03:00
Arik Fraimovich
71e125b4b0 Update Procfile.dev to use celery. 2014-07-20 12:08:08 +03:00
Arik Fraimovich
6a8befc641 Merge pull request #239 from EverythingMe/feature_outdated_queries_monitor
Model and import script for events
2014-07-09 18:55:53 +03:00
Arik Fraimovich
a79aa382d7 command to import events 2014-07-09 18:33:29 +03:00
Arik Fraimovich
5698f9692a Events model 2014-07-09 18:33:21 +03:00
Arik Fraimovich
b2381f6933 Merge pull request #238 from EverythingMe/feature_outdated_queries_monitor
Show outdated queries count and queue size in status
2014-07-08 21:51:13 +03:00
Arik Fraimovich
9a732a4dbf Show outdated queries count and queue size in status 2014-07-08 18:54:25 +03:00
Arik Fraimovich
17eb7e4146 Fix: when updating visualization need to ignore query_id 2014-07-07 16:59:18 +03:00
Arik Fraimovich
16a6c96c22 Use correct instance of queryResult 2014-07-06 18:34:26 +03:00
Arik Fraimovich
bc0a5160ac Fix: view going into infinite loop of calling getQueryResult. 2014-07-06 18:17:23 +03:00
Arik Fraimovich
62ab1fda80 Fix: UI hanging when saving query.
Clone query object, before modifying/sending over the wire.
2014-07-06 14:38:37 +03:00
Arik Fraimovich
b5309833ee Add logging to saveQuery 2014-07-06 13:59:51 +03:00
Arik Fraimovich
7b932507a6 Merge pull request #237 from EverythingMe/feature_column_editor
Feature: chart editor (no more "::x", "::y", "::series") + a lot more
2014-07-05 12:50:18 +03:00
Arik Fraimovich
c9fda5e6f1 Improve layout 2014-07-05 12:19:59 +03:00
Arik Fraimovich
a274bde092 Fix: after saving the column type mapping is empty 2014-07-05 12:19:48 +03:00
Arik Fraimovich
b4024ec880 Settings for chart options. 2014-07-05 12:02:51 +03:00
Arik Fraimovich
6367943d31 Make sure all paths of getQueryResult return same object. 2014-07-05 12:02:51 +03:00
Arik Fraimovich
eaa83556c3 Settings for second y axis. 2014-07-05 12:02:51 +03:00
Arik Fraimovich
7e720bcecd Chart columns type mapping. 2014-07-05 12:02:51 +03:00
Arik Fraimovich
003c285d11 Fix: dashboard view event 2014-07-05 12:02:51 +03:00
Arik Fraimovich
54687e72bd Merge pull request #236 from EverythingMe/fix_234
Fix #234: when converting value to moment, also set the column type
2014-07-05 11:37:00 +03:00
Arik Fraimovich
8c59386dc9 Fix #234: when converting value to moment, also set the column type 2014-07-05 11:35:10 +03:00
Arik Fraimovich
0369c557a4 Merge pull request #235 from shayel/master
Add Emacs (The One True Editor(TM)) backup files to .gitignore
2014-06-30 13:56:08 +03:00
Shay Elkin
1ca95dc497 Add Emacs (The One True Editor(TM)) backup files to .gitignore 2014-06-30 13:53:20 +03:00
Arik Fraimovich
85ea9060b0 Merge pull request #232 from jeremi/feature-bigquery-types
Add support for types in BigQuery
2014-06-27 16:31:29 +03:00
Arik Fraimovich
19b4ec7102 Merge pull request #233 from jeremi/fix-boolean-support-table
when the value is false, display false instead of empty cell
2014-06-27 16:29:46 +03:00
jeremi
b2fea7f2fe Add support for timestamps
Fix the type field
2014-06-27 15:48:52 +08:00
jeremi
d5947669ab when the value is false, display false instead of empty cell 2014-06-27 15:43:30 +08:00
jeremi
4cb97db98e Add support for types in BigQuery 2014-06-25 18:05:34 +08:00
Arik Fraimovich
9b5d43067a Revert "Merge pull request #231 from erans/master"
This introduced some unicode issues. Reverting until resolved.

This reverts commit 8731a8d273, reversing
changes made to 90157157df.
2014-06-24 14:00:21 +03:00
Arik Fraimovich
8731a8d273 Merge pull request #231 from erans/master
Force the use of JSON in Celery
2014-06-24 12:47:19 +03:00
Eran Sandler
08a06b0792 only use json in celery for serialization. pickle is going to be deprecated soon 2014-06-24 12:29:44 +03:00
Arik Fraimovich
90157157df Merge pull request #229 from jeremi/fix-heroku-procfile
fix starting of celery in Heroku
2014-06-24 11:24:54 +03:00
Arik Fraimovich
f5ea1f1559 Merge pull request #230 from jeremi/fix-default-groups
Add default group when user is created
2014-06-24 11:24:20 +03:00
jeremi
cf89e6b184 Make sure when users are created that it is with the default groups and not permissions. 2014-06-24 09:54:22 +08:00
jeremi
5920747122 fix starting of celery in Heroku 2014-06-24 09:46:40 +08:00
Arik Fraimovich
2fff4f4036 Merge pull request #227 from EverythingMe/feature_celery_status
Show Celery Flower in an iframe.
2014-06-20 15:32:49 +03:00
Arik Fraimovich
442ece5a4f Show celery flower url inside an iframe. 2014-06-20 15:29:02 +03:00
Arik Fraimovich
4bbf04b68a Update migration for new structure 2014-06-18 20:24:53 +03:00
Arik Fraimovich
f74af231ce Merge pull request #226 from EverythingMe/feature_toggle_series
Progress indicator for requests and reload on failure for dashboards
2014-06-11 18:06:26 +03:00
Arik Fraimovich
ffa679e04b Add reload in case of error for dashboards 2014-06-11 17:54:42 +03:00
Arik Fraimovich
8f1d267c00 Add pace, to indicate while things are loading (#24) 2014-06-11 17:54:21 +03:00
Arik Fraimovich
af61517384 Merge pull request #225 from hailocab/spelling-mistake-1
Spelling mistake 1
2014-06-10 15:53:38 +03:00
Zach Yewman
15a7374a4b Fixed spelling mistake of "Visualation" to "Visualization" 2014-06-10 12:55:03 +01:00
Arik Fraimovich
c0fe4a7c84 Merge pull request #224 from EverythingMe/feature_toggle_series
Feature: additional chart controls - toggle all, show total
2014-06-10 09:31:38 +03:00
Arik Fraimovich
2a18c4493b Update to latest pivottable. (fixes #211) 2014-06-10 09:27:14 +03:00
Arik Fraimovich
fc60c1b86a Additional chart controls: toggle all, show total 2014-06-10 09:26:59 +03:00
Arik Fraimovich
5b998269b3 Merge pull request #222 from EverythingMe/feature_download_from_dashboard
Feature download from dashboard
2014-05-19 16:31:53 +03:00
Arik Fraimovich
914378cc65 Remove debug printing 2014-05-19 16:29:31 +03:00
Arik Fraimovich
30f98e9796 Feature: download dataset button in dashboard 2014-05-19 16:28:25 +03:00
Arik Fraimovich
2b524075d9 Fix: indention 2014-05-19 16:16:04 +03:00
Arik Fraimovich
3641e332b0 Merge pull request #221 from EverythingMe/ui_fixes
Several bug fixes (#211, #209 and more)
2014-05-18 17:01:11 +03:00
Arik Fraimovich
4ce3f4eaa9 Include data source id in job "lock" 2014-05-18 16:35:47 +03:00
Arik Fraimovich
0b173e67a5 When changing data source, save query only if it was saved 2014-05-18 16:29:01 +03:00
Arik Fraimovich
2af234d180 Reset new dashboard form after saving (fixes #209) 2014-05-18 15:44:23 +03:00
Arik Fraimovich
d751fd8c8c Make sure table/pivot doesn't overflow 2014-05-18 15:01:40 +03:00
Arik Fraimovich
35552f9b77 Update to latest pivottable. (fixes #211) 2014-05-18 15:00:55 +03:00
Arik Fraimovich
1cc36b481a When formatting datge/time, if value is null, ignore 2014-05-18 14:36:46 +03:00
Arik Fraimovich
c9b95bc359 Fix: if column named only ::x the table was broken 2014-05-18 14:34:13 +03:00
Arik Fraimovich
86d64c35ab Add favicon to the project 2014-05-18 14:28:08 +03:00
Arik Fraimovich
8712c8567c Add /\ to escpaed characters in column name 2014-05-18 14:25:54 +03:00
Arik Fraimovich
b0cc646b5e Merge pull request #220 from EverythingMe/celery
Reconnect to database on every task.
2014-05-18 13:59:42 +03:00
Arik Fraimovich
8e1c852b0d Reset the database lock if pid changed 2014-05-18 13:57:08 +03:00
Arik Fraimovich
349f67337d Merge pull request #219 from EverythingMe/celery
Split __init__ into several modules and remove flask-peewee dependency.
2014-05-18 10:21:38 +03:00
Arik Fraimovich
4af979d3eb Split __init__ into several modules and remove flask-peewee dependency.
This should make imports more sensible and with less side effects. Also might reduce the memory footprint of the workers.
2014-05-18 10:19:07 +03:00
Arik Fraimovich
727cc67f19 Merge pull request #218 from EverythingMe/celery
Fix: queries were enqueued more than once because lock wasn't saved
2014-05-17 18:16:09 +03:00
Arik Fraimovich
f51df00564 Fix: queries were enqueued >1 because lock wasn't saved 2014-05-17 18:12:39 +03:00
Arik Fraimovich
8d7044a81a Merge pull request #217 from EverythingMe/celery
Use celery to replace our home grown background workers
2014-05-17 17:21:55 +03:00
Arik Fraimovich
d1c62b106d Fix: refresh fails if no status was previously set 2014-05-17 17:17:18 +03:00
Arik Fraimovich
a1dcf94d4d Update tests 2014-05-17 17:11:46 +03:00
Arik Fraimovich
53fc9bbf54 Use data source's queue name. 2014-05-17 16:50:44 +03:00
Arik Fraimovich
7755e9859d Add queue name to data source 2014-05-17 16:44:30 +03:00
Arik Fraimovich
21f3a80940 Use cls instead of explicit class name 2014-05-17 16:22:55 +03:00
Arik Fraimovich
06910d9002 Remove unused dependencies. 2014-05-17 16:19:32 +03:00
Arik Fraimovich
5777070bec Schedule refresh_queries using celery_beat. 2014-05-16 18:36:42 +03:00
Arik Fraimovich
8e3adcd283 Update .gitignore. 2014-05-16 18:36:21 +03:00
Arik Fraimovich
381ab62505 Move outdated queries selection logic to model. 2014-05-16 18:34:53 +03:00
Arik Fraimovich
93491004e2 Fix test due to refactor. 2014-05-16 18:19:01 +03:00
Arik Fraimovich
d1f0ae9538 Remove tests for old Job class. 2014-05-16 18:14:42 +03:00
Arik Fraimovich
94bb55d66b Remove the data.Manager as it's not needed anymore. 2014-05-16 18:13:37 +03:00
Arik Fraimovich
9de6996dc8 Deprecate old runworkers command. 2014-05-16 17:57:43 +03:00
Arik Fraimovich
9636359497 Update controllers to use new Job class. 2014-05-16 17:57:14 +03:00
Arik Fraimovich
9a6b40aff9 Enqueue jobs to celery. 2014-05-16 17:56:57 +03:00
Arik Fraimovich
82dee49a43 Remove old workers code. 2014-05-16 17:56:28 +03:00
Arik Fraimovich
9b4482f25d Move result storing logic to models. 2014-05-16 17:56:04 +03:00
Arik Fraimovich
4caf1ac3d3 Create celery app object. 2014-05-16 17:54:14 +03:00
Arik Fraimovich
0cda4a6632 Bump version to 0.4 2014-05-16 14:37:20 +03:00
Arik Fraimovich
a80618fbe2 Celery related settings 2014-05-16 14:37:11 +03:00
Arik Fraimovich
310808f1fb Add celery to requirements.txt 2014-05-16 14:26:54 +03:00
Christopher Valles
939168773a Merge remote-tracking branch 'upstream/master' 2014-05-14 11:10:43 +01:00
Arik Fraimovich
c6a415535e Merge pull request #213 from EverythingMe/feature_auto_links
Feature: auto link URLs in table
2014-05-13 20:17:26 +03:00
Arik Fraimovich
ce87c7b736 Apply cell contents as html. 2014-05-13 20:15:00 +03:00
Arik Fraimovich
036eb46ea4 Apply linking filter to string columns. 2014-05-13 20:14:45 +03:00
Arik Fraimovich
95ad15057b Filter to convert URLs into <a> elements. 2014-05-13 20:14:23 +03:00
Arik Fraimovich
459309ee4e Merge branch 'feature_group_permissions' (updated version of #208) 2014-05-13 19:42:04 +03:00
Arik Fraimovich
4e0069810e Bump version to 0.3.7 2014-05-13 19:41:50 +03:00
Arik Fraimovich
5a62e90f17 Fix migration code 2014-05-13 19:36:04 +03:00
Arik Fraimovich
cf689c424f Fix user creation in manage.py 2014-05-13 18:34:19 +03:00
Arik Fraimovich
dad9eb21a0 Create user groups in test setup. 2014-05-13 18:29:59 +03:00
Arik Fraimovich
8b581368dc Use the User.permissions property instead of groups. 2014-05-13 18:29:39 +03:00
Arik Fraimovich
ca093ec235 Move permissions logic back to a property on User model. 2014-05-13 18:18:10 +03:00
Arik Fraimovich
c6e210f107 Use new SQLMetadata class to check table permissions. 2014-05-13 18:17:39 +03:00
Arik Fraimovich
e2d0285496 Feature flag for enabling table permission checking. 2014-05-13 18:17:08 +03:00
Arik Fraimovich
16125327b1 Class for SQL metadata logic (tables, ddl, dml statements) 2014-05-13 18:16:30 +03:00
Arik Fraimovich
d8d666c971 Update the migration to use the admin permission too 2014-05-13 17:13:05 +03:00
Arik Fraimovich
772ea94b59 Fix: move the groups creation to init function 2014-05-13 16:47:58 +03:00
Yosi Taguri
e499e8099d aligned the file. added sleep to job status retry and fixed the error message when HttpError is raised 2014-05-13 16:26:17 +03:00
Yosi Taguri
75bc9bb318 support monitoring a long running job. 2014-05-13 16:26:16 +03:00
Christopher Valles
f79362c7a3 Merge remote-tracking branch 'upstream/master' 2014-05-13 14:06:37 +01:00
Arik Fraimovich
2c34ecde35 Merge pull request #210 from yosit/master-yosit
support monitoring a long running job.
2014-05-13 09:07:11 +03:00
Yosi Taguri
1610d9b782 aligned the file. added sleep to job status retry and fixed the error message when HttpError is raised 2014-05-13 09:03:30 +03:00
Yosi Taguri
17dd4efb27 support monitoring a long running job. 2014-05-12 19:31:29 +03:00
Christopher Valles
7a2af73bea Fix table case sensitive for permissions 2014-05-12 14:15:11 +01:00
Christopher Valles
81d027611f Remove is_admin flag 2014-05-09 18:12:34 +01:00
Christopher Valles
9ef941bc63 Adding default groups to manage.py database create_tables command 2014-05-09 18:02:22 +01:00
Christopher Valles
cb0d27e691 Fix errors 2014-05-09 14:44:26 +01:00
Christopher Valles
03767bbc0a Adding logging for permission denied situations 2014-05-09 13:19:23 +01:00
Christopher Valles
0042b73cd9 Fixes 2014-05-09 13:06:01 +01:00
Christopher Valles
1c095bcd99 Fix tests 2014-05-09 12:43:29 +01:00
Christopher Valles
4287d9a2e2 Remove gemfiles from gitignore 2014-05-08 19:54:12 +01:00
Christopher Valles
e297faab7c Fixing more tests 2014-05-08 19:29:50 +01:00
Christopher Valles
c0329cc0ef Fixed tests; 2014-05-08 19:19:23 +01:00
Christopher Valles
dc7050d4ef Fix manage.py permission reference 2014-05-08 18:43:59 +01:00
Christopher Valles
3a2f2be95d Merge stuff 2014-05-08 18:38:44 +01:00
Christopher Valles
b4432ee21d Merge branch 'master' of https://github.com/hailocab/redash 2014-05-08 18:22:40 +01:00
Christopher Valles
d9b0e84bbe Remove ADMIN env variable 2014-05-08 12:40:58 +01:00
Arik Fraimovich
e8c946b88b Merge pull request #205 from joeysim/keyboard-shortcut
added support for cmd+enter execution
2014-05-08 10:57:30 +03:00
Joey Simhon
7b94260135 added support for cmd+enter execution 2014-05-07 22:45:39 +03:00
Christopher Valles
51c59dad63 Put back jquery-ui reference 2014-05-07 17:00:52 +01:00
Christopher Valles
2d398696d0 Update from upstream 2014-05-07 15:36:42 +01:00
Christopher Valles
ceb08808f8 Merge pull request #5 from hailocab/permission_system
Permission system
2014-05-07 15:28:52 +01:00
Arik Fraimovich
e7c6ba8c1d Merge pull request #204 from EverythingMe/performance
Add Bucky (client side metrics client).
2014-05-07 17:28:19 +03:00
Arik Fraimovich
3cee9c9b3a Merge pull request #204 from EverythingMe/performance
Add Bucky (client side metrics client).
2014-05-07 17:28:19 +03:00
Arik Fraimovich
509edf651b Add bucky (client side metrics client). 2014-05-07 17:25:43 +03:00
Arik Fraimovich
28224a0ba1 Add bucky (client side metrics client). 2014-05-07 17:25:43 +03:00
Christopher Valles
4e8cd93905 Fix conflict 2014-05-07 15:24:49 +01:00
Christopher Valles
069fe38354 Merge pull request #2 from hailocab/add/requirement
Add gunicorn to the requirements
2014-05-07 15:20:15 +01:00
Arik Fraimovich
05c915cf00 Fix indendentation 2014-05-07 15:48:29 +03:00
Arik Fraimovich
37512b5fdd Fix indendentation 2014-05-07 15:48:29 +03:00
Arik Fraimovich
0fa22500be Merge pull request #203 from EverythingMe/performance
Report to statsd request render time
2014-05-07 15:15:56 +03:00
Arik Fraimovich
3fbc73d181 Merge pull request #203 from EverythingMe/performance
Report to statsd request render time
2014-05-07 15:15:56 +03:00
Arik Fraimovich
4d4f41733d Report to statsd request render time 2014-05-07 15:13:29 +03:00
Arik Fraimovich
113821cc97 Report to statsd request render time 2014-05-07 15:13:29 +03:00
Christopher Valles
3f9ba7ff00 Fix cohort visualization 2014-05-06 14:28:35 +01:00
Arik Fraimovich
37bf79c9eb Merge pull request #201 from EverythingMe/feature_dashboard_filters
Use column type data (if available) to properly render data table.
2014-05-05 19:48:05 +03:00
Arik Fraimovich
073deb8315 Merge pull request #201 from EverythingMe/feature_dashboard_filters
Use column type data (if available) to properly render data table.
2014-05-05 19:48:05 +03:00
Arik Fraimovich
38293fc155 Fix: query save fails if query has queryResult property. 2014-05-05 19:45:07 +03:00
Arik Fraimovich
7793b3fe41 Fix: query save fails if query has queryResult property. 2014-05-05 19:45:07 +03:00
Arik Fraimovich
52f44588e6 Use column type (if available) to better render tables. 2014-05-05 19:44:52 +03:00
Arik Fraimovich
25de0303a1 Use column type (if available) to better render tables. 2014-05-05 19:44:52 +03:00
Arik Fraimovich
0ffda9d002 Populate the column type field. 2014-05-05 19:44:28 +03:00
Arik Fraimovich
a37aa11baf Populate the column type field. 2014-05-05 19:44:28 +03:00
Arik Fraimovich
e7331633a4 Fix indentation. 2014-05-05 19:32:17 +03:00
Arik Fraimovich
1ae40981fe Fix indentation. 2014-05-05 19:32:17 +03:00
Arik Fraimovich
19743f387b Merge pull request #200 from EverythingMe/feature_dashboard_filters
Feature: dashboard filters
2014-05-05 18:49:52 +03:00
Arik Fraimovich
17bb5eac91 Merge pull request #200 from EverythingMe/feature_dashboard_filters
Feature: dashboard filters
2014-05-05 18:49:52 +03:00
Arik Fraimovich
77d628d2db Support for dashboard filters in the UI. 2014-05-05 18:46:38 +03:00
Arik Fraimovich
e5348bcf9f Support for dashboard filters in the UI. 2014-05-05 18:46:38 +03:00
Arik Fraimovich
bcce69904d Global filters flag for dashboard. 2014-05-05 18:42:49 +03:00
Arik Fraimovich
ee7e452c70 Global filters flag for dashboard. 2014-05-05 18:42:49 +03:00
Arik Fraimovich
7b4c04024c Use new getQuery accessor. 2014-05-05 18:36:12 +03:00
Arik Fraimovich
73402a4f3c Use new getQuery accessor. 2014-05-05 18:36:12 +03:00
Arik Fraimovich
a40da45b1e Show filters in dashboards (if available). 2014-05-05 18:35:07 +03:00
Arik Fraimovich
42a3309731 Show filters in dashboards (if available). 2014-05-05 18:35:07 +03:00
Arik Fraimovich
638fb123ec Query: cache QueryResult so each call gets the same one. 2014-05-05 18:34:54 +03:00
Arik Fraimovich
f2e06e6191 Query: cache QueryResult so each call gets the same one. 2014-05-05 18:34:54 +03:00
Arik Fraimovich
f95a09a015 Widget: accessor function to get Query object. 2014-05-05 18:34:29 +03:00
Arik Fraimovich
a10a38575b Widget: accessor function to get Query object. 2014-05-05 18:34:29 +03:00
Arik Fraimovich
b74f4639a0 Merge pull request #199 from EverythingMe/feature_dashboard_filters
Fix: set was messing up column order
2014-05-04 16:02:53 +03:00
Arik Fraimovich
c7efe3a99f Merge pull request #199 from EverythingMe/feature_dashboard_filters
Fix: set was messing up column order
2014-05-04 16:02:53 +03:00
Arik Fraimovich
a7b10db3f4 Fix: set was messing up column order 2014-05-04 16:00:57 +03:00
Arik Fraimovich
cc544e9343 Fix: set was messing up column order 2014-05-04 16:00:57 +03:00
Arik Fraimovich
0a301bd997 Merge pull request #198 from EverythingMe/fixes
Fix version name in tarball
2014-05-04 15:01:18 +03:00
Arik Fraimovich
2abffff9fd Merge pull request #198 from EverythingMe/fixes
Fix version name in tarball
2014-05-04 15:01:18 +03:00
Arik Fraimovich
174eb2408e Fix version name in tarball 2014-05-04 15:00:20 +03:00
Arik Fraimovich
e91c9a00b1 Fix version name in tarball 2014-05-04 15:00:20 +03:00
Arik Fraimovich
3b6af18009 Merge pull request #197 from EverythingMe/fixes
Small fixes (show version in admin/status, open the visualization editor by default in new visualizations)
2014-05-04 14:27:44 +03:00
Arik Fraimovich
c9608dfa4f Merge pull request #197 from EverythingMe/fixes
Small fixes (show version in admin/status, open the visualization editor by default in new visualizations)
2014-05-04 14:27:44 +03:00
Arik Fraimovich
ab2fa1e352 Show version in admin/status 2014-05-04 14:25:20 +03:00
Arik Fraimovich
bd0b5c7136 Show version in admin/status 2014-05-04 14:25:20 +03:00
Arik Fraimovich
9a025a7e05 Fix the pack make command 2014-05-04 14:02:20 +03:00
Arik Fraimovich
d198a99419 Fix the pack make command 2014-05-04 14:02:20 +03:00
Arik Fraimovich
96081de51f update makefile to set version" 2014-05-04 13:46:11 +03:00
Arik Fraimovich
16c461c15f update makefile to set version" 2014-05-04 13:46:11 +03:00
Arik Fraimovich
1bf56899f3 Open visualization editor when adding a new visualization. 2014-05-04 13:23:18 +03:00
Arik Fraimovich
c874a2218b Open visualization editor when adding a new visualization. 2014-05-04 13:23:18 +03:00
Arik Fraimovich
79b4c86520 Improve latest_release utility 2014-05-04 13:14:49 +03:00
Arik Fraimovich
d92d994532 Improve latest_release utility 2014-05-04 13:14:49 +03:00
Christopher Valles
1704914d6b Add stuff to gitignore 2014-05-01 18:36:01 +01:00
Arik Fraimovich
9c43b55668 Merge pull request #196 from EverythingMe/fixes
Bug fixes (#91, #195)
2014-05-01 17:56:34 +03:00
Arik Fraimovich
cddd7e909d Merge pull request #196 from EverythingMe/fixes
Bug fixes (#91, #195)
2014-05-01 17:56:34 +03:00
Arik Fraimovich
9a6852db78 Fix #195: When two columns have the same name their values get overriden 2014-05-01 17:52:42 +03:00
Arik Fraimovich
2270042c0f Fix #195: When two columns have the same name their values get overriden 2014-05-01 17:52:42 +03:00
Arik Fraimovich
6ae3a7552a Fix #91: better filtering of bad tokens in column names 2014-05-01 17:45:35 +03:00
Arik Fraimovich
8e5e37ee1b Fix #91: better filtering of bad tokens in column names 2014-05-01 17:45:35 +03:00
Christopher Valles
146131761f DAT-768 2014-04-30 17:04:24 +01:00
Arik Fraimovich
855aecd85f Merge pull request #194 from EverythingMe/feature_markdown_widget
Several small fixes (#186, #120, #174)
2014-04-29 16:06:38 +03:00
Arik Fraimovich
cdf6a1994b Merge pull request #194 from EverythingMe/feature_markdown_widget
Several small fixes (#186, #120, #174)
2014-04-29 16:06:38 +03:00
Arik Fraimovich
a7ce5246a6 Fix: return last cached result for ttl=-1 (fix #174) 2014-04-29 16:02:17 +03:00
Arik Fraimovich
6efd830bd4 Fix: return last cached result for ttl=-1 (fix #174) 2014-04-29 16:02:17 +03:00
Arik Fraimovich
a8ea811fed Make job expiry time configurable. 2014-04-29 12:13:33 +03:00
Arik Fraimovich
f39a848aa2 Make job expiry time configurable. 2014-04-29 12:13:33 +03:00
Arik Fraimovich
a71b99a873 Workaround for cases when widget is missing but referenced in a dashboard layout (re. #120) 2014-04-29 12:09:38 +03:00
Arik Fraimovich
9f2fc1f90a Workaround for cases when widget is missing but referenced in a dashboard layout (re. #120) 2014-04-29 12:09:38 +03:00
Arik Fraimovich
391c220604 Show error message if failed deleting a visualization 2014-04-29 11:57:16 +03:00
Arik Fraimovich
fd9d71b927 Show error message if failed deleting a visualization 2014-04-29 11:57:16 +03:00
Arik Fraimovich
e5bf431987 Fix: Chart type resets to Date/Time when editing #186 2014-04-29 11:37:42 +03:00
Arik Fraimovich
ba8a39db57 Fix: Chart type resets to Date/Time when editing #186 2014-04-29 11:37:42 +03:00
Arik Fraimovich
f23b434972 Merge pull request #192 from EverythingMe/feature_markdown_widget
Feature: text box widget that supports markdown
2014-04-29 11:30:32 +03:00
Arik Fraimovich
191ad19cac Merge pull request #192 from EverythingMe/feature_markdown_widget
Feature: text box widget that supports markdown
2014-04-29 11:30:32 +03:00
Arik Fraimovich
ef366df1fb Remove unused dependencies 2014-04-29 11:23:32 +03:00
Arik Fraimovich
14112fd45b Remove unused dependencies 2014-04-29 11:23:32 +03:00
Arik Fraimovich
2caf02b4e0 Markdown support for textbox. 2014-04-29 11:15:19 +03:00
Arik Fraimovich
676cf32c22 Markdown support for textbox. 2014-04-29 11:15:19 +03:00
Arik Fraimovich
b7a0b7454a Add textbox widget support. 2014-04-29 10:36:56 +03:00
Arik Fraimovich
289d38b2a6 Add textbox widget support. 2014-04-29 10:36:56 +03:00
Arik Fraimovich
fa2986a154 Add underscore.string lib. 2014-04-29 10:36:00 +03:00
Arik Fraimovich
850ac9f4c8 Add underscore.string lib. 2014-04-29 10:36:00 +03:00
Arik Fraimovich
084e9f8394 Migration to add text column to widgets and make visualization_id nullable. 2014-04-29 10:35:47 +03:00
Arik Fraimovich
4ffd21be09 Migration to add text column to widgets and make visualization_id nullable. 2014-04-29 10:35:47 +03:00
Christopher Valles
3e87fff8b1 Merge branch 'master' into permission_system 2014-04-28 17:57:43 +01:00
Christopher Valles
a37c1eb589 Merge pull request #4 from hailocab/capistrano
cap initial commit - DON'T MERGE
2014-04-28 17:56:41 +01:00
Arik Fraimovich
7d0324be91 Merge pull request #181 from EverythingMe/feature_imrpove_updater
Switch to multiprocessing instead of threading
2014-04-27 18:21:50 +03:00
Arik Fraimovich
63c85deb5c Merge pull request #181 from EverythingMe/feature_imrpove_updater
Switch to multiprocessing instead of threading
2014-04-27 18:21:50 +03:00
Arik Fraimovich
2938e57980 Cleaner shutdown (#8) 2014-04-27 18:14:15 +03:00
Arik Fraimovich
ac89584083 Cleaner shutdown (#8) 2014-04-27 18:14:15 +03:00
Arik Fraimovich
413dd61491 Remove atfork (it's not needed anymore) 2014-04-27 18:13:43 +03:00
Arik Fraimovich
74f9d85752 Remove atfork (it's not needed anymore) 2014-04-27 18:13:43 +03:00
Arik Fraimovich
08d6a90469 Switch to multiprocessing instead of threading. 2014-04-27 18:13:43 +03:00
Arik Fraimovich
b85c535c6f Switch to multiprocessing instead of threading. 2014-04-27 18:13:43 +03:00
Christopher Valles
f50799cc7b Working on permissions 2014-04-25 16:44:33 +01:00
Arik Fraimovich
e8aba6b682 Merge pull request #191 from EverythingMe/fix_185_null_values
Fix: job variable was used before assignment
2014-04-25 17:21:36 +03:00
Arik Fraimovich
a2dbc76116 Merge pull request #191 from EverythingMe/fix_185_null_values
Fix: job variable was used before assignment
2014-04-25 17:21:36 +03:00
Arik Fraimovich
163ee33ae6 Fix: job variable was used before assignment 2014-04-25 17:18:39 +03:00
Arik Fraimovich
83933e24ac Fix: job variable was used before assignment 2014-04-25 17:18:39 +03:00
Arik Fraimovich
a9f24669b7 Merge pull request #190 from EverythingMe/fix_185_null_values
Fix #185: when y value is null, convert it to 0.
2014-04-25 17:16:56 +03:00
Arik Fraimovich
638df29d95 Merge pull request #190 from EverythingMe/fix_185_null_values
Fix #185: when y value is null, convert it to 0.
2014-04-25 17:16:56 +03:00
Arik Fraimovich
73d99031b7 Fix #185: when y value is null, convert it to 0. 2014-04-25 17:14:05 +03:00
Arik Fraimovich
2e01d57c9b Fix #185: when y value is null, convert it to 0. 2014-04-25 17:14:05 +03:00
Arik Fraimovich
6f6c1678ff Merge pull request #182 from EverythingMe/feature_events_throtle
Make sure events are reported at most once per second
2014-04-22 16:40:34 +03:00
Arik Fraimovich
d26b822f6c Merge pull request #182 from EverythingMe/feature_events_throtle
Make sure events are reported at most once per second
2014-04-22 16:40:34 +03:00
Arik Fraimovich
976dc1e496 Report event for viewing widget/visualization/query 2014-04-22 16:35:40 +03:00
Arik Fraimovich
c49fbe1ac2 Report event for viewing widget/visualization/query 2014-04-22 16:35:40 +03:00
Arik Fraimovich
6a7e322b97 Report events at most once per second 2014-04-22 16:35:22 +03:00
Arik Fraimovich
4b6b1984aa Report events at most once per second 2014-04-22 16:35:22 +03:00
Arik Fraimovich
0e564bc8f8 Merge pull request #180 from EverythingMe/feature_imrpove_updater
Fix: support for .env files without EXPORT
2014-04-20 09:51:27 +03:00
Arik Fraimovich
8a546b4193 Merge pull request #180 from EverythingMe/feature_imrpove_updater
Fix: support for .env files without EXPORT
2014-04-20 09:51:27 +03:00
Arik Fraimovich
6fe733aeaa Fix: support for .env files without EXPORT 2014-04-20 09:43:17 +03:00
Arik Fraimovich
31c09dd7ce Fix: support for .env files without EXPORT 2014-04-20 09:43:17 +03:00
Arik Fraimovich
af18670131 Merge pull request #179 from EverythingMe/feature_imrpove_updater
CLI for data sources management
2014-04-19 17:22:46 +03:00
Arik Fraimovich
98f0bc0188 Merge pull request #179 from EverythingMe/feature_imrpove_updater
CLI for data sources management
2014-04-19 17:22:46 +03:00
Arik Fraimovich
362e5b820e CLI for data sources. 2014-04-19 17:06:47 +03:00
Arik Fraimovich
36d27dfd74 CLI for data sources. 2014-04-19 17:06:47 +03:00
Arik Fraimovich
2204c437a2 Merge pull request #178 from EverythingMe/feature_imrpove_updater
Typo fix in import code.
2014-04-19 15:41:49 +03:00
Arik Fraimovich
9edd8313ec Merge pull request #178 from EverythingMe/feature_imrpove_updater
Typo fix in import code.
2014-04-19 15:41:49 +03:00
Arik Fraimovich
95bcffc28a Use exec in bin/run. 2014-04-19 15:39:26 +03:00
Arik Fraimovich
790cbd95b1 Use exec in bin/run. 2014-04-19 15:39:26 +03:00
Arik Fraimovich
efdaf4cf3a Typo fix. 2014-04-19 15:39:09 +03:00
Arik Fraimovich
5dd8b102e1 Typo fix. 2014-04-19 15:39:09 +03:00
Arik Fraimovich
04d92ce14b Merge pull request #176 from EverythingMe/feature_imrpove_updater
Fix: selection of true/false values in filters wasn't working.
2014-04-16 17:01:21 +03:00
Arik Fraimovich
43496ecdb2 Merge pull request #176 from EverythingMe/feature_imrpove_updater
Fix: selection of true/false values in filters wasn't working.
2014-04-16 17:01:21 +03:00
Arik Fraimovich
fec6c8b6a7 Fix: selection of true/false values in filters wasn't working. 2014-04-16 14:25:40 +03:00
Arik Fraimovich
ff099b4314 Fix: selection of true/false values in filters wasn't working. 2014-04-16 14:25:40 +03:00
Christopher Valles
78da5ae92e First refactor of permissions; 2014-04-14 18:14:01 +01:00
Arik Fraimovich
6ab4c4551a Fix upload version script. 2014-04-13 16:49:57 +03:00
Arik Fraimovich
59a8c0c2c2 Fix upload version script. 2014-04-13 16:49:57 +03:00
Arik Fraimovich
851c080c13 Merge pull request #172 from EverythingMe/feature_imrpove_updater
Feature imrpove updater
2014-04-13 16:38:14 +03:00
Arik Fraimovich
cb800c5907 Merge pull request #172 from EverythingMe/feature_imrpove_updater
Feature imrpove updater
2014-04-13 16:38:14 +03:00
Arik Fraimovich
0daf715152 Utility to get last redash release url 2014-04-13 16:35:45 +03:00
Arik Fraimovich
31cc6fdaeb Utility to get last redash release url 2014-04-13 16:35:45 +03:00
Arik Fraimovich
e335398ba7 Set select2 options via object 2014-04-13 16:11:42 +03:00
Arik Fraimovich
1a8611a3c0 Set select2 options via object 2014-04-13 16:11:42 +03:00
Arik Fraimovich
8178900d56 Copy select2 assets, as the grunt pipeline skips them 2014-04-13 16:11:21 +03:00
Arik Fraimovich
258e3c957d Copy select2 assets, as the grunt pipeline skips them 2014-04-13 16:11:21 +03:00
Arik Fraimovich
9f9d78fd7a Update upload script to include checksums 2014-04-13 14:14:55 +03:00
Arik Fraimovich
1d83021ab3 Update upload script to include checksums 2014-04-13 14:14:55 +03:00
Arik Fraimovich
d9af5d3943 Fix: user should be able to cancel query even if process not existing already (#8). 2014-04-12 16:43:40 +03:00
Arik Fraimovich
7ed9dc90d3 Fix: user should be able to cancel query even if process not existing already (#8). 2014-04-12 16:43:40 +03:00
Arik Fraimovich
433e004295 Set TTL on finishsed jobs (fix #106) 2014-04-12 16:36:20 +03:00
Arik Fraimovich
f3628f7bba Set TTL on finishsed jobs (fix #106) 2014-04-12 16:36:20 +03:00
Christopher Valles
314a75f8a2 Update from upstream + adding venv to gitignore 2014-04-10 16:05:07 +01:00
Arik Fraimovich
185b1c9df0 Merge pull request #170 from EverythingMe/feature_usage_tracking
Feature: basic usage tracking
2014-04-10 16:11:10 +03:00
Arik Fraimovich
a686baa372 Merge pull request #170 from EverythingMe/feature_usage_tracking
Feature: basic usage tracking
2014-04-10 16:11:10 +03:00
Arik Fraimovich
881e44fbb6 Fix: access query after it's assigned 2014-04-10 15:10:39 +03:00
Arik Fraimovich
a4518dc2aa Fix: access query after it's assigned 2014-04-10 15:10:39 +03:00
Arik Fraimovich
d7e1328fc0 Make it possible to log events to stdout without logging to file. 2014-04-10 13:04:03 +03:00
Arik Fraimovich
9b8c3872c6 Make it possible to log events to stdout without logging to file. 2014-04-10 13:04:03 +03:00
Arik Fraimovich
2c7a6004c0 Pass timestamp with event. 2014-04-10 13:02:52 +03:00
Arik Fraimovich
5a0f524b5e Pass timestamp with event. 2014-04-10 13:02:52 +03:00
Arik Fraimovich
6d62f0d2c9 Setup events logging from settings. 2014-04-10 13:02:40 +03:00
Arik Fraimovich
0551e992fa Setup events logging from settings. 2014-04-10 13:02:40 +03:00
Arik Fraimovich
8615429e0c Logging setup for events. 2014-04-10 13:02:24 +03:00
Arik Fraimovich
1b0d315b30 Logging setup for events. 2014-04-10 13:02:24 +03:00
Arik Fraimovich
bd67c2ff21 Bump version (about time...) 2014-04-10 12:55:31 +03:00
Arik Fraimovich
577fdffc7f Bump version (about time...) 2014-04-10 12:55:31 +03:00
Arik Fraimovich
65e8bef22c Improved logging output. 2014-04-10 12:55:02 +03:00
Arik Fraimovich
241d31f608 Improved logging output. 2014-04-10 12:55:02 +03:00
Arik Fraimovich
c84f18449b Events end point. 2014-04-10 12:29:21 +03:00
Arik Fraimovich
57a23a1181 Events end point. 2014-04-10 12:29:21 +03:00
Arik Fraimovich
718577f565 Events reporting from client side. 2014-04-10 12:29:07 +03:00
Arik Fraimovich
c2e4e19004 Events reporting from client side. 2014-04-10 12:29:07 +03:00
Christopher Valles
69f14c3a61 Merge remote-tracking branch 'upstream/master' 2014-04-10 10:15:55 +01:00
Arik Fraimovich
52441ec5b4 Merge pull request #169 from EverythingMe/feature_filter_imporvements
Feature: improved ::filter
2014-04-09 16:30:45 +03:00
Arik Fraimovich
fcda122107 Merge pull request #169 from EverythingMe/feature_filter_imporvements
Feature: improved ::filter
2014-04-09 16:30:45 +03:00
Arik Fraimovich
01b908539b Show filter name 2014-04-09 16:27:16 +03:00
Arik Fraimovich
d7f6b589cd Show filter name 2014-04-09 16:27:16 +03:00
Arik Fraimovich
eca62cd1f2 Use select2 for filters, for autocomplete and multiple selection (#161, #160) 2014-04-09 15:25:49 +03:00
Arik Fraimovich
4de9bf2d61 Use select2 for filters, for autocomplete and multiple selection (#161, #160) 2014-04-09 15:25:49 +03:00
Arik Fraimovich
67ec5614e1 Add multi-filter option (#161) 2014-04-09 15:25:22 +03:00
Arik Fraimovich
599f12fdc2 Add multi-filter option (#161) 2014-04-09 15:25:22 +03:00
Arik Fraimovich
a92ef02b07 Add select2 to the project 2014-04-09 15:24:33 +03:00
Arik Fraimovich
18d16bb92d Add select2 to the project 2014-04-09 15:24:33 +03:00
Arik Fraimovich
45d11d3227 Merge pull request #167 from EverythingMe/fix_small_stuff
Fix: pie charts display (all categories were named "Slice X")
2014-04-08 11:21:26 +03:00
Arik Fraimovich
26365054bf Merge pull request #167 from EverythingMe/fix_small_stuff
Fix: pie charts display (all categories were named "Slice X")
2014-04-08 11:21:26 +03:00
Arik Fraimovich
3cefa004cd Fix pie charts display 2014-04-08 11:20:52 +03:00
Arik Fraimovich
58a22c0a97 Fix pie charts display 2014-04-08 11:20:52 +03:00
Arik Fraimovich
d3852db164 Merge pull request #166 from EverythingMe/fix_small_stuff
Control over xAxis type & fix for a bug when deleting a visualization
2014-04-07 21:02:55 +03:00
Arik Fraimovich
cce4a08b54 Merge pull request #166 from EverythingMe/fix_small_stuff
Control over xAxis type & fix for a bug when deleting a visualization
2014-04-07 21:02:55 +03:00
Arik Fraimovich
b242295de0 Feature: Control over xAxis type. 2014-04-07 20:50:46 +03:00
Arik Fraimovich
f80a940ff4 Feature: Control over xAxis type. 2014-04-07 20:50:46 +03:00
Arik Fraimovich
a37142426c Fix: when deleting visualization it would fail because DEFAULT_TAB is undefined 2014-04-07 20:50:06 +03:00
Arik Fraimovich
794d8ddfcf Fix: when deleting visualization it would fail because DEFAULT_TAB is undefined 2014-04-07 20:50:06 +03:00
Arik Fraimovich
271d577074 Merge pull request #165 from erans/master
Make sure qr serialization will always be in JSON
2014-04-07 13:44:21 +03:00
Arik Fraimovich
7adf4bf763 Merge pull request #165 from erans/master
Make sure qr serialization will always be in JSON
2014-04-07 13:44:21 +03:00
Eran Sandler
2fd3033418 Make sure qr serialization will always be in JSON - in the case we do end up serializing big objects - so that other parts of the system can be written in languages other than Python 2014-04-07 12:14:10 +03:00
Eran Sandler
e50aa536c2 Make sure qr serialization will always be in JSON - in the case we do end up serializing big objects - so that other parts of the system can be written in languages other than Python 2014-04-07 12:14:10 +03:00
Arik Fraimovich
74de143636 Merge pull request #164 from EverythingMe/feature_view_query_permission
Feature: "view_query" permission
2014-04-06 20:31:20 +03:00
Arik Fraimovich
2d3348b1a9 Merge pull request #164 from EverythingMe/feature_view_query_permission
Feature: "view_query" permission
2014-04-06 20:31:20 +03:00
Arik Fraimovich
81ca8b9012 More control over creating users from CLI 2014-04-06 20:26:35 +03:00
Arik Fraimovich
df733d3e9c More control over creating users from CLI 2014-04-06 20:26:35 +03:00
Arik Fraimovich
0167bebf04 Create stub User object for API to use permissions model 2014-04-06 20:05:43 +03:00
Arik Fraimovich
b1d6a5a45a Create stub User object for API to use permissions model 2014-04-06 20:05:43 +03:00
Arik Fraimovich
5de1795380 Don't show links to queries in the UI. 2014-04-06 19:32:46 +03:00
Arik Fraimovich
3bb26c5906 Don't show links to queries in the UI. 2014-04-06 19:32:46 +03:00
Arik Fraimovich
99a9fdde25 Use view_query permission in controllers 2014-04-06 19:16:30 +03:00
Arik Fraimovich
e2f9b7565b Use view_query permission in controllers 2014-04-06 19:16:30 +03:00
Arik Fraimovich
3e6dd8e929 Migration for new permission 2014-04-06 19:16:18 +03:00
Arik Fraimovich
6556f22e91 Migration for new permission 2014-04-06 19:16:18 +03:00
Arik Fraimovich
c0fc7c8222 new permission: view_query 2014-04-06 19:16:10 +03:00
Arik Fraimovich
e5377abf0f new permission: view_query 2014-04-06 19:16:10 +03:00
Amir Nissim
1eb2d562a5 Merge pull request #156 from EverythingMe/90-ui-issues
90 ui issues
2014-04-03 15:21:54 +03:00
Amir Nissim
b4625f1c78 Merge pull request #156 from EverythingMe/90-ui-issues
90 ui issues
2014-04-03 15:21:54 +03:00
Amir Nissim
82f5f15c2a [#90] edit vis. form touchup 2014-04-03 15:14:27 +03:00
Amir Nissim
63037c62a0 [#90] edit vis. form touchup 2014-04-03 15:14:27 +03:00
Amir Nissim
a696e10ef7 [#90] visualization edit mode 2014-04-03 15:05:17 +03:00
Amir Nissim
617bbc213f [#90] visualization edit mode 2014-04-03 15:05:17 +03:00
Amir Nissim
87933bd8ac rename: QueryEditCtrl -> QuerySourceCtrl
'edit' is confusing since it is also possible to make changes in the QueryViewCtrl
2014-04-03 15:05:17 +03:00
Amir Nissim
9e3cb6e581 rename: QueryEditCtrl -> QuerySourceCtrl
'edit' is confusing since it is also possible to make changes in the QueryViewCtrl
2014-04-03 15:05:17 +03:00
Amir Nissim
29f01a5780 [#90] clear visualization hash when redirecting to forked query 2014-04-03 15:05:17 +03:00
Amir Nissim
d4dfc67059 [#90] clear visualization hash when redirecting to forked query 2014-04-03 15:05:17 +03:00
Arik Fraimovich
23a3a7f20e Merge pull request #157 from EverythingMe/fix_dashboard_watch
Fix: some dashboards get into infinite loop of watches
2014-03-30 18:03:48 +03:00
Arik Fraimovich
5ec2d2fe97 Merge pull request #157 from EverythingMe/fix_dashboard_watch
Fix: some dashboards get into infinite loop of watches
2014-03-30 18:03:48 +03:00
Arik Fraimovich
b2e7813d87 Fix: some dashboards get into infinite loop of watches 2014-03-30 17:30:32 +03:00
Arik Fraimovich
0b093415ca Fix: some dashboards get into infinite loop of watches 2014-03-30 17:30:32 +03:00
Arik Fraimovich
ff9fadd55a Merge pull request #154 from EverythingMe/90-ui-issues
[#90] save only modified fields when changing query name/description
2014-03-26 18:02:38 +02:00
Arik Fraimovich
77f226e4a2 Merge pull request #154 from EverythingMe/90-ui-issues
[#90] save only modified fields when changing query name/description
2014-03-26 18:02:38 +02:00
Amir Nissim
40adba4242 [#90] query-link: use ng-href as @arikfr suggested 2014-03-26 17:52:07 +02:00
Amir Nissim
71a4d5288d [#90] query-link: use ng-href as @arikfr suggested 2014-03-26 17:52:07 +02:00
Amir Nissim
d4d118af17 update angular-resource (adds .$promise support) 2014-03-26 17:27:18 +02:00
Amir Nissim
72c74101da update angular-resource (adds .$promise support) 2014-03-26 17:27:18 +02:00
Amir Nissim
ace657d95a [#90] handle widget creation failures 2014-03-26 16:42:56 +02:00
Amir Nissim
1bb12b87ac [#90] handle widget creation failures 2014-03-26 16:42:56 +02:00
Amir Nissim
fd3e9e3fcb query links: no underline 2014-03-26 13:30:50 +02:00
Amir Nissim
ec40436a65 query links: no underline 2014-03-26 13:30:50 +02:00
Amir Nissim
3243f277f2 [#90] query link: style, link to #table, 'query' attr required 2014-03-26 13:25:21 +02:00
Amir Nissim
7cd129db52 [#90] query link: style, link to #table, 'query' attr required 2014-03-26 13:25:21 +02:00
Amir Nissim
7ac76c2996 dashboard_directives.js 2014-03-26 12:19:02 +02:00
Amir Nissim
904c54003d dashboard_directives.js 2014-03-26 12:19:02 +02:00
Amir Nissim
84b0590ec5 move DashboardCtrl and WidgetCtrl to dashboard.js 2014-03-26 12:14:23 +02:00
Amir Nissim
ba63048fc0 move DashboardCtrl and WidgetCtrl to dashboard.js 2014-03-26 12:14:23 +02:00
Arik Fraimovich
a46c651dad Merge pull request #155 from EverythingMe/vis-fix
[#144] Allow users to edit raw JSON visualization options
2014-03-26 10:06:04 +02:00
Arik Fraimovich
ecb80df10a Merge pull request #155 from EverythingMe/vis-fix
[#144] Allow users to edit raw JSON visualization options
2014-03-26 10:06:04 +02:00
Amir Nissim
11ba93cc80 [#90] query links in widget title 2014-03-25 17:58:38 +02:00
Amir Nissim
782919788d [#90] query links in widget title 2014-03-25 17:58:38 +02:00
Amir Nissim
23760ffa86 [#90] switch to new visualization tab on save 2014-03-25 17:29:19 +02:00
Amir Nissim
37dbdf494f [#90] switch to new visualization tab on save 2014-03-25 17:29:19 +02:00
Amir Nissim
5ad2bd048c [#90] perf: don't render (ngIf) the table visualization tab instead of hiding (ngHide) 2014-03-25 17:12:54 +02:00
Amir Nissim
9717a686be [#90] perf: don't render (ngIf) the table visualization tab instead of hiding (ngHide) 2014-03-25 17:12:54 +02:00
Amir Nissim
839abe627e [#144] Allow users to edit raw JSON visualization options 2014-03-25 17:06:28 +02:00
Amir Nissim
55167adef6 [#144] Allow users to edit raw JSON visualization options 2014-03-25 17:06:28 +02:00
Amir Nissim
9305b76b85 [#90] save only modified fields when changing query name/description 2014-03-25 15:25:37 +02:00
Amir Nissim
001e2a8887 [#90] save only modified fields when changing query name/description 2014-03-25 15:25:37 +02:00
Arik Fraimovich
61a196fafc Merge pull request #150 from EverythingMe/query-refactor
#138: Query controllers refactor
2014-03-25 14:34:15 +02:00
Arik Fraimovich
a503e20c92 Merge pull request #150 from EverythingMe/query-refactor
#138: Query controllers refactor
2014-03-25 14:34:15 +02:00
Arik Fraimovich
0a05d31b17 Fix: only save query on meta+s if user can edit query 2014-03-25 14:30:07 +02:00
Arik Fraimovich
80a5804c9c Fix: only save query on meta+s if user can edit query 2014-03-25 14:30:07 +02:00
Amir Nissim
001950a116 Revert "Navigation service"
This reverts commit 3dc8d9a842.
2014-03-25 14:11:29 +02:00
Amir Nissim
89cbaf0ac5 Revert "Navigation service"
This reverts commit 3dc8d9a842.
2014-03-25 14:11:29 +02:00
Amir Nissim
3670c7c3a7 [#138] onQuerySave callback 2014-03-25 14:04:18 +02:00
Amir Nissim
f2f61a1fc9 [#138] onQuerySave callback 2014-03-25 14:04:18 +02:00
Amir Nissim
3dc8d9a842 Navigation service 2014-03-25 12:28:18 +02:00
Amir Nissim
b93132e5d9 Navigation service 2014-03-25 12:28:18 +02:00
Amir Nissim
fbb8943eeb [#138] update queryText when query is saved 2014-03-25 11:38:52 +02:00
Amir Nissim
156bf96788 [#138] update queryText when query is saved 2014-03-25 11:38:52 +02:00
Arik Fraimovich
84d07903f6 Merge pull request #153 from EverythingMe/feature_data_source
Shell wrapper to source env before running command
2014-03-25 10:25:12 +02:00
Arik Fraimovich
4d1908dceb Merge pull request #153 from EverythingMe/feature_data_source
Shell wrapper to source env before running command
2014-03-25 10:25:12 +02:00
Arik Fraimovich
1571676d7a Shell wrapper to source env before running command 2014-03-25 10:22:50 +02:00
Arik Fraimovich
870cc142a9 Shell wrapper to source env before running command 2014-03-25 10:22:50 +02:00
Arik Fraimovich
8cb0472497 Add manage.py command to print settings 2014-03-25 10:22:21 +02:00
Arik Fraimovich
eade74ffb0 Add manage.py command to print settings 2014-03-25 10:22:21 +02:00
Arik Fraimovich
de41dc84af Remove migrate make command 2014-03-25 10:19:11 +02:00
Arik Fraimovich
880412da94 Remove migrate make command 2014-03-25 10:19:11 +02:00
Arik Fraimovich
5ae2b88cec Merge pull request #152 from EverythingMe/feature_data_source
Make task to run a migration
2014-03-25 09:34:19 +02:00
Arik Fraimovich
a9dae21483 Merge pull request #152 from EverythingMe/feature_data_source
Make task to run a migration
2014-03-25 09:34:19 +02:00
Arik Fraimovich
0a22fb61dc Make task to run a migration 2014-03-25 09:33:44 +02:00
Arik Fraimovich
0578273f7e Make task to run a migration 2014-03-25 09:33:44 +02:00
Amir Nissim
5d37f1a34b KeyboardShortcuts service 2014-03-24 17:51:57 +02:00
Amir Nissim
cf9fe300fe KeyboardShortcuts service 2014-03-24 17:51:57 +02:00
Amir Nissim
bbe17f3a09 [#138] fix ui-codemirror bug when used as directive 2014-03-24 16:13:59 +02:00
Amir Nissim
1bea6a9627 [#138] fix ui-codemirror bug when used as directive 2014-03-24 16:13:59 +02:00
Amir Nissim
21ad5bbb4a [#138] simplify saveQuery, drop $route dependency 2014-03-24 16:07:24 +02:00
Amir Nissim
5ce4fcb974 [#138] simplify saveQuery, drop $route dependency 2014-03-24 16:07:24 +02:00
Amir Nissim
977193b009 fix getColumns failures when QueryResult has no data 2014-03-24 14:33:03 +02:00
Amir Nissim
028a3e9d62 fix getColumns failures when QueryResult has no data 2014-03-24 14:33:03 +02:00
Amir Nissim
16a83f6134 getQueryResult only if query.data_source_id exists 2014-03-24 14:33:03 +02:00
Amir Nissim
fa2438f40d getQueryResult only if query.data_source_id exists 2014-03-24 14:33:03 +02:00
Amir Nissim
e0af1f20af [#138] cherry pick rebase conflicts:
366cdbf616 Remove reference to query result when changing data source
 872cee2228 Unless data source set already, set it to the first one.
 8ae41c0b6a Show query's data source.
2014-03-24 14:33:02 +02:00
Amir Nissim
10bccfb4ad [#138] cherry pick rebase conflicts:
366cdbf616 Remove reference to query result when changing data source
 872cee2228 Unless data source set already, set it to the first one.
 8ae41c0b6a Show query's data source.
2014-03-24 14:33:02 +02:00
Amir Nissim
ca415c50ad [#138] store original query text to detect changes 2014-03-24 14:33:02 +02:00
Amir Nissim
3c0972b8ac [#138] store original query text to detect changes 2014-03-24 14:33:02 +02:00
Amir Nissim
c4cbe06c12 [#138] Query.newQuery 2014-03-24 14:33:02 +02:00
Amir Nissim
98ac23a843 [#138] Query.newQuery 2014-03-24 14:33:02 +02:00
Amir Nissim
34fb58d403 typo: cancelInterval -> clearInterval 2014-03-24 14:33:02 +02:00
Amir Nissim
df458c1052 typo: cancelInterval -> clearInterval 2014-03-24 14:33:02 +02:00
Amir Nissim
cddf69e422 [#138] rebase fixes (cherry pick f3d4635) 2014-03-24 14:33:02 +02:00
Amir Nissim
dd86711b32 [#138] rebase fixes (cherry pick f3d4635) 2014-03-24 14:33:02 +02:00
Amir Nissim
6a1c5aeae7 [#138] move saveQuery to ViewCtrl 2014-03-24 14:33:02 +02:00
Amir Nissim
4493d22ec9 [#138] move saveQuery to ViewCtrl 2014-03-24 14:33:02 +02:00
Amir Nissim
f3411a46a5 [#138] alert-unsaved-changes directive 2014-03-24 14:33:02 +02:00
Amir Nissim
5ffd2615e7 [#138] alert-unsaved-changes directive 2014-03-24 14:33:02 +02:00
Amir Nissim
7616738fc6 [#138] QueryEditCtrl cleanup 2014-03-24 14:33:02 +02:00
Amir Nissim
e996b4fa22 [#138] QueryEditCtrl cleanup 2014-03-24 14:33:02 +02:00
Amir Nissim
5d03ce6b50 [#138] QueryViewCtrl cleanup and formatting 2014-03-24 14:33:02 +02:00
Amir Nissim
bcca2aa341 [#138] QueryViewCtrl cleanup and formatting 2014-03-24 14:33:02 +02:00
Amir Nissim
3ad8114a28 [#138] query directives 2014-03-24 14:33:02 +02:00
Amir Nissim
602d935559 [#138] query directives 2014-03-24 14:33:02 +02:00
Amir Nissim
37d56a2bf6 [#138] editCtrl inheriting viewCtrl 2014-03-24 14:33:02 +02:00
Amir Nissim
af9318fbd1 [#138] editCtrl inheriting viewCtrl 2014-03-24 14:33:02 +02:00
Arik Fraimovich
cff07a3e3d Merge pull request #151 from EverythingMe/feature_data_source
Fix issue with serializing unicode queries.
2014-03-24 14:31:13 +02:00
Arik Fraimovich
2ba4bcd98e Merge pull request #151 from EverythingMe/feature_data_source
Fix issue with serializing unicode queries.
2014-03-24 14:31:13 +02:00
Arik Fraimovich
a1f81705dd Unicode test case for Job 2014-03-24 14:26:57 +02:00
Arik Fraimovich
fac9082a03 Unicode test case for Job 2014-03-24 14:26:57 +02:00
Arik Fraimovich
b8dba48759 Fix issue with serializing unicode queries 2014-03-24 14:18:03 +02:00
Arik Fraimovich
9ac335116c Fix issue with serializing unicode queries 2014-03-24 14:18:03 +02:00
Arik Fraimovich
ae8706ab85 Merge pull request #149 from EverythingMe/feature_data_source
Feature: Support multiple data sources (databases) for querying (#12)
2014-03-23 17:02:55 +02:00
Arik Fraimovich
fbc325bf07 Merge pull request #149 from EverythingMe/feature_data_source
Feature: Support multiple data sources (databases) for querying (#12)
2014-03-23 17:02:55 +02:00
Arik Fraimovich
af85943c08 Add comment about moving logic to the model 2014-03-23 12:52:22 +02:00
Arik Fraimovich
cad34f63bf Add comment about moving logic to the model 2014-03-23 12:52:22 +02:00
Arik Fraimovich
d7a453e8b1 Fix tests (were rightfully failing on system with clock set to utc) 2014-03-20 20:57:35 +02:00
Arik Fraimovich
d9964d84b3 Fix tests (were rightfully failing on system with clock set to utc) 2014-03-20 20:57:35 +02:00
Arik Fraimovich
725a8f2bb5 Reverse comparison 2014-03-20 20:16:23 +02:00
Arik Fraimovich
9379f76562 Reverse comparison 2014-03-20 20:16:23 +02:00
Arik Fraimovich
5979d91875 Reduce Peewee's logging level to INFO in tests. 2014-03-20 19:45:16 +02:00
Arik Fraimovich
21e02ee04e Reduce Peewee's logging level to INFO in tests. 2014-03-20 19:45:16 +02:00
Arik Fraimovich
86b95a404a Apply filters only when available 2014-03-20 19:38:05 +02:00
Arik Fraimovich
214806d31b Apply filters only when available 2014-03-20 19:38:05 +02:00
Arik Fraimovich
366cdbf616 Remove reference to query result when changing data source 2014-03-20 19:30:05 +02:00
Arik Fraimovich
cea1a73ad6 Remove reference to query result when changing data source 2014-03-20 19:30:05 +02:00
Arik Fraimovich
addaf97489 Add results verification 2014-03-20 18:49:38 +02:00
Arik Fraimovich
e37fa7e5a0 Add results verification 2014-03-20 18:49:38 +02:00
Arik Fraimovich
6989c7d2fd Script to test concurrency issues 2014-03-20 18:16:09 +02:00
Arik Fraimovich
b079b27875 Script to test concurrency issues 2014-03-20 18:16:09 +02:00
Arik Fraimovich
166b1a7c6b Switch to using peewee models in Manager + fix bugs + add tests (#8). 2014-03-20 13:22:37 +02:00
Arik Fraimovich
3c895310f4 Switch to using peewee models in Manager + fix bugs + add tests (#8). 2014-03-20 13:22:37 +02:00
Arik Fraimovich
2d3a0cc917 Update peewee version to be able to use window functions. 2014-03-20 13:18:32 +02:00
Arik Fraimovich
ae9e80d6a8 Update peewee version to be able to use window functions. 2014-03-20 13:18:32 +02:00
Arik Fraimovich
f58ffd884b Remove data.manager.QueryResult class. 2014-03-20 10:47:07 +02:00
Arik Fraimovich
9f0abd0bc6 Remove data.manager.QueryResult class. 2014-03-20 10:47:07 +02:00
Arik Fraimovich
afb1b3f16f Merge pull request #147 from EverythingMe/fix_141_digest_called_every_second
Fix #141: prevent the timer directive call digest loop every second
2014-03-20 09:33:46 +02:00
Arik Fraimovich
3bedfe75a8 Merge pull request #147 from EverythingMe/fix_141_digest_called_every_second
Fix #141: prevent the timer directive call digest loop every second
2014-03-20 09:33:46 +02:00
Arik Fraimovich
93f87f0922 Fix #141: prevent the timer directive call digest loop every second
By using setInterval & $scope.$digest instead of $timeout which uses
$scope.$apply, which in turn calls $rootScope.$digest.
2014-03-20 09:29:27 +02:00
Arik Fraimovich
76ce8b0876 Fix #141: prevent the timer directive call digest loop every second
By using setInterval & $scope.$digest instead of $timeout which uses
$scope.$apply, which in turn calls $rootScope.$digest.
2014-03-20 09:29:27 +02:00
Arik Fraimovich
872cee2228 Unless data source set already, set it to the first one. 2014-03-20 09:21:01 +02:00
Arik Fraimovich
fcebbb4856 Unless data source set already, set it to the first one. 2014-03-20 09:21:01 +02:00
Arik Fraimovich
99b7e3126b When updating query result, set the data source id. 2014-03-20 09:20:41 +02:00
Arik Fraimovich
1b02f58247 When updating query result, set the data source id. 2014-03-20 09:20:41 +02:00
Arik Fraimovich
8d8dafade3 Allow updating data source when updating query. 2014-03-20 09:20:27 +02:00
Arik Fraimovich
687b3be784 Allow updating data source when updating query. 2014-03-20 09:20:27 +02:00
Arik Fraimovich
ee3150fc6b Update query results for same data source only 2014-03-19 14:58:13 +02:00
Arik Fraimovich
4922be1422 Update query results for same data source only 2014-03-19 14:58:13 +02:00
Arik Fraimovich
515eb28d4d No need to pass connection string to workers 2014-03-19 13:52:19 +02:00
Arik Fraimovich
062e65732a No need to pass connection string to workers 2014-03-19 13:52:19 +02:00
Arik Fraimovich
f186c8cb5f Remove get_query_result_by_id from data.Manager. 2014-03-19 13:50:52 +02:00
Arik Fraimovich
c40a73726e Remove get_query_result_by_id from data.Manager. 2014-03-19 13:50:52 +02:00
Arik Fraimovich
193587dcfb Move QUeyrResult logic from data.Manager to QueryResult. 2014-03-19 13:48:48 +02:00
Arik Fraimovich
e8d453e2d4 Move QUeyrResult logic from data.Manager to QueryResult. 2014-03-19 13:48:48 +02:00
Arik Fraimovich
3f91ebea5f Fix QueryResult factory. 2014-03-19 13:48:11 +02:00
Arik Fraimovich
0c4d0cb5c5 Fix QueryResult factory. 2014-03-19 13:48:11 +02:00
Arik Fraimovich
7f118635b4 Fix import job to use data source with query result. 2014-03-19 13:47:35 +02:00
Arik Fraimovich
7efa48b3d7 Fix import job to use data source with query result. 2014-03-19 13:47:35 +02:00
Arik Fraimovich
0c199431a9 Add data source to QueryResult 2014-03-19 12:57:42 +02:00
Arik Fraimovich
000c482f1b Add data source to QueryResult 2014-03-19 12:57:42 +02:00
Arik Fraimovich
4fffcab8aa Fix tests to use data source 2014-03-19 12:53:51 +02:00
Arik Fraimovich
c919648412 Fix tests to use data source 2014-03-19 12:53:51 +02:00
Arik Fraimovich
7eb849affb Data Source factory 2014-03-19 12:53:39 +02:00
Arik Fraimovich
6b57d4a2f7 Data Source factory 2014-03-19 12:53:39 +02:00
Arik Fraimovich
579ca28d6d Fix importer to use data source 2014-03-19 12:53:30 +02:00
Arik Fraimovich
21b52e0b80 Fix importer to use data source 2014-03-19 12:53:30 +02:00
Arik Fraimovich
679921dc8e Add DataSource to models list 2014-03-19 11:45:38 +02:00
Arik Fraimovich
7bd5604607 Add DataSource to models list 2014-03-19 11:45:38 +02:00
Arik Fraimovich
259ea39d55 Move Highcharts color definitions to highchart's code file 2014-03-19 11:44:50 +02:00
Arik Fraimovich
bb83157cbe Move Highcharts color definitions to highchart's code file 2014-03-19 11:44:50 +02:00
Arik Fraimovich
f637ddf8ca Remove definition of QueryFIddleCtrl. 2014-03-19 11:42:36 +02:00
Arik Fraimovich
ca7af014ae Remove definition of QueryFIddleCtrl. 2014-03-19 11:42:36 +02:00
Arik Fraimovich
08b92e1f3d Remove QueryFiddle ctrl. 2014-03-19 11:39:08 +02:00
Arik Fraimovich
a429487894 Remove QueryFiddle ctrl. 2014-03-19 11:39:08 +02:00
Arik Fraimovich
d4e4afb97d Put deprecation comment for data source settings. 2014-03-19 11:37:07 +02:00
Arik Fraimovich
12f2dc8795 Put deprecation comment for data source settings. 2014-03-19 11:37:07 +02:00
Arik Fraimovich
dad207912e Fix: query wasn't saving. 2014-03-19 11:34:26 +02:00
Arik Fraimovich
ec76ea307f Fix: query wasn't saving. 2014-03-19 11:34:26 +02:00
Arik Fraimovich
6c9322624d Use datasource when executing queries. 2014-03-19 11:23:38 +02:00
Arik Fraimovich
499909e09e Use datasource when executing queries. 2014-03-19 11:23:38 +02:00
Arik Fraimovich
8ae41c0b6a Show query's data source. 2014-03-19 11:22:51 +02:00
Arik Fraimovich
baad4742ef Show query's data source. 2014-03-19 11:22:51 +02:00
Arik Fraimovich
b6dbc3356d dict representation for DataSource. 2014-03-19 11:22:15 +02:00
Arik Fraimovich
a8773a9582 dict representation for DataSource. 2014-03-19 11:22:15 +02:00
Arik Fraimovich
2e078294c9 Update angular-resource to 1.2.7 2014-03-19 11:19:43 +02:00
Arik Fraimovich
efbb78ad7f Update angular-resource to 1.2.7 2014-03-19 11:19:43 +02:00
Arik Fraimovich
1d001407a0 Move query runner creation to worker based on data source in Job. 2014-03-18 20:45:03 +02:00
Arik Fraimovich
8d41180f4c Move query runner creation to worker based on data source in Job. 2014-03-18 20:45:03 +02:00
Arik Fraimovich
0b994de531 Refactor Job class to be easier to extend.
Moved the Redis logic out of it.
2014-03-18 17:48:37 +02:00
Arik Fraimovich
5a07ac38da Refactor Job class to be easier to extend.
Moved the Redis logic out of it.
2014-03-18 17:48:37 +02:00
Arik Fraimovich
caa198964c Move logging setup to __init__.py so it's always available 2014-03-18 17:48:37 +02:00
Arik Fraimovich
163f483a56 Move logging setup to __init__.py so it's always available 2014-03-18 17:48:37 +02:00
Arik Fraimovich
c7ded66057 Data sources model 2014-03-18 17:48:37 +02:00
Arik Fraimovich
e2ce0809da Data sources model 2014-03-18 17:48:37 +02:00
Arik Fraimovich
8c80e99d3b Merge pull request #139 from erans/master
Added support for running scripts as queries
2014-03-18 17:45:09 +02:00
Arik Fraimovich
bea85d0f62 Merge pull request #139 from erans/master
Added support for running scripts as queries
2014-03-18 17:45:09 +02:00
Christopher Valles
f87119e31a Merge pull request #3 from hailocab/fix/bower
Fix ECMDERR
2014-03-18 12:08:36 +00:00
Arik Fraimovich
3f2ac6ab76 Merge pull request #143 from EverythingMe/fix_stacking
Feature: import query from json file
2014-03-17 21:29:49 +02:00
Arik Fraimovich
6a5b3a89d9 Merge pull request #143 from EverythingMe/fix_stacking
Feature: import query from json file
2014-03-17 21:29:49 +02:00
Arik Fraimovich
b97c9ee3c9 Feature: import query from json file 2014-03-17 21:28:48 +02:00
Arik Fraimovich
48b0c60cf1 Feature: import query from json file 2014-03-17 21:28:48 +02:00
Arik Fraimovich
f9fbff3fa5 Merge pull request #142 from EverythingMe/fix_stacking
Fix stacking
2014-03-17 21:24:39 +02:00
Arik Fraimovich
9b31e193ee Merge pull request #142 from EverythingMe/fix_stacking
Fix stacking
2014-03-17 21:24:39 +02:00
Arik Fraimovich
cdac5fbf52 Remove console.log 2014-03-17 21:24:18 +02:00
Arik Fraimovich
20d12c0498 Remove console.log 2014-03-17 21:24:18 +02:00
Arik Fraimovich
aa7e010342 Fix: when having categories chart and not all series had values
it wouldn't draw the chart with stacking other than none.
2014-03-17 21:22:07 +02:00
Arik Fraimovich
fec57ecf59 Fix: when having categories chart and not all series had values
it wouldn't draw the chart with stacking other than none.
2014-03-17 21:22:07 +02:00
Arik Fraimovich
74d667b942 Merge pull request #140 from EverythingMe/query_filters
Feature: filters for all visualizations and not only tables
2014-03-17 21:08:58 +02:00
Arik Fraimovich
1c52d533d4 Merge pull request #140 from EverythingMe/query_filters
Feature: filters for all visualizations and not only tables
2014-03-17 21:08:58 +02:00
Arik Fraimovich
9a04535e6b Reset filterFreeze when updating data. 2014-03-17 20:25:58 +02:00
Arik Fraimovich
c26fdb5dad Reset filterFreeze when updating data. 2014-03-17 20:25:58 +02:00
Arik Fraimovich
f3d46355af Because we draw the table without VisualizationRenderer we need to explicitly add filters here too. 2014-03-17 20:23:12 +02:00
Arik Fraimovich
db35b6f4e8 Because we draw the table without VisualizationRenderer we need to explicitly add filters here too. 2014-03-17 20:23:12 +02:00
Arik Fraimovich
44621e4f37 Switch to $watchCollection to resolve the issue of chart not updating when length of series stays the same. 2014-03-17 20:22:49 +02:00
Arik Fraimovich
690d4b8f50 Switch to $watchCollection to resolve the issue of chart not updating when length of series stays the same. 2014-03-17 20:22:49 +02:00
Arik Fraimovich
a99e290bc5 Store filters on QueryResult object and use them in getQueryData. 2014-03-17 20:22:24 +02:00
Arik Fraimovich
5b0f124307 Store filters on QueryResult object and use them in getQueryData. 2014-03-17 20:22:24 +02:00
Arik Fraimovich
2b5291900d Show filters directive on all visualizations. 2014-03-17 20:21:39 +02:00
Arik Fraimovich
cc9d10b12b Show filters directive on all visualizations. 2014-03-17 20:21:39 +02:00
Arik Fraimovich
19209d16aa Filters directive. 2014-03-17 20:20:49 +02:00
Arik Fraimovich
5ee924a770 Filters directive. 2014-03-17 20:20:49 +02:00
Eran Sandler
a2257999a7 moved to use the query_runner.annotate_query flag so we won't get the SQL comment 2014-03-17 18:56:50 +02:00
Eran Sandler
d6337ec472 moved to use the query_runner.annotate_query flag so we won't get the SQL comment 2014-03-17 18:56:50 +02:00
Eran Sandler
d3e87a3d28 added support for a 'url' source where you can supply a URL to retrieve the same JSON result used in other query runners 2014-03-17 18:44:31 +02:00
Eran Sandler
05f1a6b7ea added support for a 'url' source where you can supply a URL to retrieve the same JSON result used in other query runners 2014-03-17 18:44:31 +02:00
Eran Sandler
d435d122eb Added support for running scripts as queries 2014-03-17 16:36:51 +02:00
Eran Sandler
dc364981c8 Added support for running scripts as queries 2014-03-17 16:36:51 +02:00
Arik Fraimovich
362c899632 Merge pull request #136 from EverythingMe/logout
Logout button
2014-03-13 12:37:06 +02:00
Arik Fraimovich
dd8478fe0a Merge pull request #136 from EverythingMe/logout
Logout button
2014-03-13 12:37:06 +02:00
Amir Nissim
a80ed6998e logout button. closes #125 2014-03-13 12:29:25 +02:00
Amir Nissim
97d614659a logout button. closes #125 2014-03-13 12:29:25 +02:00
Arik Fraimovich
c7540ba87b Fix minification issue 2014-03-12 16:53:22 +02:00
Arik Fraimovich
3b11f010b5 Fix minification issue 2014-03-12 16:53:22 +02:00
Alessio Garofalo
06e282102c Fix ECMDERR 2014-03-12 12:42:37 +00:00
Arik Fraimovich
0b0d2bcdfc Reduce expire time to 1800, to reduce changes of test failing 2014-03-12 13:41:42 +02:00
Arik Fraimovich
607123e67a Reduce expire time to 1800, to reduce changes of test failing 2014-03-12 13:41:42 +02:00
Arik Fraimovich
3451deee03 Merge pull request #135 from EverythingMe/feature_roles
Fix: create table only if it doesn't exists.
2014-03-12 13:35:14 +02:00
Arik Fraimovich
67e4d24c11 Merge pull request #135 from EverythingMe/feature_roles
Fix: create table only if it doesn't exists.
2014-03-12 13:35:14 +02:00
Arik Fraimovich
2d995d0935 Create table only if it doesn't exists 2014-03-12 13:32:28 +02:00
Arik Fraimovich
0e3c6ac275 Create table only if it doesn't exists 2014-03-12 13:32:28 +02:00
Arik Fraimovich
3b34b1c2d9 Merge pull request #134 from EverythingMe/feature_roles
Feature: control the system name
2014-03-12 13:24:45 +02:00
Arik Fraimovich
549f9288a1 Merge pull request #134 from EverythingMe/feature_roles
Feature: control the system name
2014-03-12 13:24:45 +02:00
Arik Fraimovich
ae3151d3a7 Feature: control the system name 2014-03-12 13:22:37 +02:00
Arik Fraimovich
86ba16fbb8 Feature: control the system name 2014-03-12 13:22:37 +02:00
Arik Fraimovich
f07428a0df Merge pull request #133 from EverythingMe/feature_roles
Feature: basic permissions system
2014-03-12 13:17:41 +02:00
Arik Fraimovich
cb74a2c6ae Merge pull request #133 from EverythingMe/feature_roles
Feature: basic permissions system
2014-03-12 13:17:41 +02:00
Arik Fraimovich
0ab59033b5 Update manage.py to use permissions 2014-03-12 13:13:06 +02:00
Arik Fraimovich
97b163bc95 Update manage.py to use permissions 2014-03-12 13:13:06 +02:00
Arik Fraimovich
09f2e89bc4 Update tests for /status.json 2014-03-12 13:08:19 +02:00
Arik Fraimovich
13f3a5e172 Update tests for /status.json 2014-03-12 13:08:19 +02:00
Arik Fraimovich
3066327b0e Use permissions in the UI 2014-03-12 12:59:05 +02:00
Arik Fraimovich
3bcd8bf2d5 Use permissions in the UI 2014-03-12 12:59:05 +02:00
Arik Fraimovich
52d7650d61 send user's permissions to the view 2014-03-12 12:46:05 +02:00
Arik Fraimovich
b0c50bd817 send user's permissions to the view 2014-03-12 12:46:05 +02:00
Arik Fraimovich
aaa38689b3 Change roles to permissions 2014-03-12 12:45:12 +02:00
Arik Fraimovich
3d95d6b8c9 Change roles to permissions 2014-03-12 12:45:12 +02:00
Arik Fraimovich
bf62b52183 Require admin role when asking for admin resource 2014-03-12 11:40:40 +02:00
Arik Fraimovich
cff710ee52 Require admin role when asking for admin resource 2014-03-12 11:40:40 +02:00
Arik Fraimovich
0961d13ac2 require_role(s) decorators 2014-03-12 11:40:40 +02:00
Arik Fraimovich
5003f36337 require_role(s) decorators 2014-03-12 11:40:40 +02:00
Arik Fraimovich
e976f39d2b Add roles field to user 2014-03-12 11:40:40 +02:00
Arik Fraimovich
2854a1c8c0 Add roles field to user 2014-03-12 11:40:40 +02:00
Arik Fraimovich
c34889ced9 Remove unneeded wrapper function. 2014-03-12 11:40:40 +02:00
Arik Fraimovich
5eeaf6853e Remove unneeded wrapper function. 2014-03-12 11:40:40 +02:00
Arik Fraimovich
a569a2c2c1 Merge pull request #132 from EverythingMe/feature_import
Fix overflow CSS to be auto instead of scroll
2014-03-12 11:38:44 +02:00
Arik Fraimovich
08b6141d06 Merge pull request #132 from EverythingMe/feature_import
Fix overflow CSS to be auto instead of scroll
2014-03-12 11:38:44 +02:00
Arik Fraimovich
356128fbf5 Fix overflow CSS to be auto instead of scroll 2014-03-12 11:38:21 +02:00
Arik Fraimovich
6cbc2736d8 Fix overflow CSS to be auto instead of scroll 2014-03-12 11:38:21 +02:00
Arik Fraimovich
a1ac2d512b Merge pull request #131 from EverythingMe/feature_import
Feature: import dashboard (along with widgets, visualization and queries) from JSON
2014-03-11 19:23:24 +02:00
Arik Fraimovich
2db600b8d7 Merge pull request #131 from EverythingMe/feature_import
Feature: import dashboard (along with widgets, visualization and queries) from JSON
2014-03-11 19:23:24 +02:00
Arik Fraimovich
c3fc9879e0 Fix: use relative file path 2014-03-11 19:15:44 +02:00
Arik Fraimovich
5df3dbde1a Fix: use relative file path 2014-03-11 19:15:44 +02:00
Arik Fraimovich
126d6f7f60 Update importer to use mappings 2014-03-11 18:51:41 +02:00
Arik Fraimovich
417571ecd6 Update importer to use mappings 2014-03-11 18:51:41 +02:00
Arik Fraimovich
3d726fe7b0 Update importer to update existing objects 2014-03-11 18:40:42 +02:00
Arik Fraimovich
6fa5668cbc Update importer to update existing objects 2014-03-11 18:40:42 +02:00
Arik Fraimovich
c6ba21ad4c Update Widget and QueryResult to inherit from BaseModel 2014-03-11 18:40:24 +02:00
Arik Fraimovich
07b8d3d157 Update Widget and QueryResult to inherit from BaseModel 2014-03-11 18:40:24 +02:00
Arik Fraimovich
be3bad7b90 Move import functions into a class, to have state 2014-03-11 18:23:25 +02:00
Arik Fraimovich
d6bd19438c Move import functions into a class, to have state 2014-03-11 18:23:25 +02:00
Arik Fraimovich
2f53c7924d Import functions to import JSON representation of a dashboard 2014-03-11 18:23:25 +02:00
Arik Fraimovich
0f29506dda Import functions to import JSON representation of a dashboard 2014-03-11 18:23:25 +02:00
Arik Fraimovich
08d46bbbe3 Merge pull request #126 from EverythingMe/query-view-page
Fixes #121: redesign query page (have separate page for editing and viewing)
2014-03-11 18:22:31 +02:00
Arik Fraimovich
f420c91909 Merge pull request #126 from EverythingMe/query-view-page
Fixes #121: redesign query page (have separate page for editing and viewing)
2014-03-11 18:22:31 +02:00
Arik Fraimovich
db94db2957 Add support for ESC key in edit-in-place 2014-03-11 18:16:15 +02:00
Arik Fraimovich
6c00b8a853 Add support for ESC key in edit-in-place 2014-03-11 18:16:15 +02:00
Arik Fraimovich
c87dcf8aac Fix tab size 2014-03-11 18:09:12 +02:00
Arik Fraimovich
38f20d7eba Fix tab size 2014-03-11 18:09:12 +02:00
Arik Fraimovich
0e1dbc9624 Change fork button to default 2014-03-11 18:08:24 +02:00
Arik Fraimovich
19b97f63e5 Change fork button to default 2014-03-11 18:08:24 +02:00
Amir Nissim
0b90b7ea79 #121 fixes:
* fork your own query
* better redirect after saving new query
* UI fixes
2014-03-11 17:16:58 +02:00
Amir Nissim
fa4258f75c #121 fixes:
* fork your own query
* better redirect after saving new query
* UI fixes
2014-03-11 17:16:58 +02:00
Amir Nissim
2b652cac1f rd-time-ago directive 2014-03-11 16:35:19 +02:00
Amir Nissim
583546a7ca rd-time-ago directive 2014-03-11 16:35:19 +02:00
Amir Nissim
6c40610d34 #121 More UI issues:
* source as link instead of button
* fix source link when url has a hash
* new query uses the new layout
* rename url /src => /source...
* when deleting a visualization update the hash
* don't submit title/description if it hasn't changed
* mobile: fix description field wrapping
2014-03-11 15:21:53 +02:00
Amir Nissim
a6f527bd51 #121 More UI issues:
* source as link instead of button
* fix source link when url has a hash
* new query uses the new layout
* rename url /src => /source...
* when deleting a visualization update the hash
* don't submit title/description if it hasn't changed
* mobile: fix description field wrapping
2014-03-11 15:21:53 +02:00
Amir Nissim
f1aec05835 #121: source button to play nice with hashes - cont'd 2014-03-11 15:20:13 +02:00
Amir Nissim
56672a862f #121: source button to play nice with hashes - cont'd 2014-03-11 15:20:13 +02:00
Arik Fraimovich
4860ea1b4e Revert to urls without slash when not needed 2014-03-11 15:20:13 +02:00
Arik Fraimovich
b5e5fb2bde Revert to urls without slash when not needed 2014-03-11 15:20:13 +02:00
Arik Fraimovich
53dcd8b7b2 Fix routes 2014-03-11 15:20:13 +02:00
Arik Fraimovich
cf82b4899a Fix routes 2014-03-11 15:20:13 +02:00
Amir Nissim
e8e2aab8e3 #121: source button to play nice with hashes 2014-03-11 15:20:13 +02:00
Amir Nissim
554b21241b #121: source button to play nice with hashes 2014-03-11 15:20:13 +02:00
Amir Nissim
8d1b523b94 #121: alerts 2014-03-11 15:20:13 +02:00
Amir Nissim
d6068395fa #121: alerts 2014-03-11 15:20:13 +02:00
Amir Nissim
31c59467db #121: editing query name, description, ttl triggers save 2014-03-11 15:20:13 +02:00
Amir Nissim
4836e5c239 #121: editing query name, description, ttl triggers save 2014-03-11 15:20:13 +02:00
Amir Nissim
54c5a7dcb3 #121: /src url 2014-03-11 15:20:13 +02:00
Amir Nissim
0ff4de1e10 #121: /src url 2014-03-11 15:20:13 +02:00
Amir Nissim
d4287558f9 #121: use resolve in RouteProvider to get query and instantiate controller when resolved 2014-03-11 15:20:12 +02:00
Amir Nissim
c91368229a #121: use resolve in RouteProvider to get query and instantiate controller when resolved 2014-03-11 15:20:12 +02:00
Amir Nissim
da496975bc #121: more layout changes 2014-03-11 15:20:12 +02:00
Amir Nissim
324205ed37 #121: more layout changes 2014-03-11 15:20:12 +02:00
Amir Nissim
aaafb0f465 #121: routing new views 2014-03-11 15:20:12 +02:00
Amir Nissim
950989b139 #121: routing new views 2014-03-11 15:20:12 +02:00
Amir Nissim
7618fc97d2 #121: mobile tweaks 2014-03-11 15:20:12 +02:00
Amir Nissim
498027301e #121: mobile tweaks 2014-03-11 15:20:12 +02:00
Amir Nissim
f01d224bdf #121: layout change - query editor on top 2014-03-11 15:20:12 +02:00
Amir Nissim
35f4be1abc #121: layout change - query editor on top 2014-03-11 15:20:12 +02:00
Amir Nissim
08355ff8af #121: edit refresh schedule, move alerts to right column 2014-03-11 15:20:12 +02:00
Amir Nissim
c9a8f7bd82 #121: edit refresh schedule, move alerts to right column 2014-03-11 15:20:12 +02:00
Arik Fraimovich
f2ebfaba3e Fix: no more flickering when switching visualization tabs. 2014-03-11 15:20:12 +02:00
Arik Fraimovich
7ad20ccff6 Fix: no more flickering when switching visualization tabs. 2014-03-11 15:20:12 +02:00
Amir Nissim
67f4c78d61 #121: editable query name and description 2014-03-11 15:20:12 +02:00
Amir Nissim
1d4d5b4c1f #121: editable query name and description 2014-03-11 15:20:12 +02:00
Amir Nissim
02cf984711 #121: 'show query' button for non-owners 2014-03-11 15:20:12 +02:00
Amir Nissim
2fa37a9732 #121: 'show query' button for non-owners 2014-03-11 15:20:12 +02:00
Amir Nissim
ef86f44215 organizing /app files 2014-03-11 15:20:12 +02:00
Amir Nissim
51db8346d3 organizing /app files 2014-03-11 15:20:12 +02:00
Amir Nissim
315803dde2 #121: QueryView page edit mode 2014-03-11 15:20:11 +02:00
Amir Nissim
e0c330fb29 #121: QueryView page edit mode 2014-03-11 15:20:11 +02:00
Amir Nissim
f8280552a0 #121: QueryViewCtrl with 'strict mode' 2014-03-11 15:20:11 +02:00
Amir Nissim
61316c40e5 #121: QueryViewCtrl with 'strict mode' 2014-03-11 15:20:11 +02:00
Amir Nissim
4adfc4353b #121 query view page 2014-03-11 15:20:11 +02:00
Amir Nissim
e57fabbd1d #121 query view page 2014-03-11 15:20:11 +02:00
Arik Fraimovich
7d9a7eafc6 Merge pull request #130 from EverythingMe/fix_category_chart_sorting
Fix: (in category charts) don't sort values when there is more than one category
2014-03-11 15:16:51 +02:00
Arik Fraimovich
6ee4e6cd8e Merge pull request #130 from EverythingMe/fix_category_chart_sorting
Fix: (in category charts) don't sort values when there is more than one category
2014-03-11 15:16:51 +02:00
Arik Fraimovich
97b727dcc0 Fix: don't sort values when there is more than one category 2014-03-11 15:14:32 +02:00
Arik Fraimovich
2cbee1bf82 Fix: don't sort values when there is more than one category 2014-03-11 15:14:32 +02:00
Arik Fraimovich
81525fa61b Fix: compile all views. 2014-03-09 12:52:33 +02:00
Arik Fraimovich
30b4628593 Fix: compile all views. 2014-03-09 12:52:33 +02:00
Arik Fraimovich
87bb092c9d Merge pull request #128 from EverythingMe/fix_visualizations_issues
Several visualizations related fixes.
2014-03-06 22:09:43 +02:00
Arik Fraimovich
5e72cc61b6 Merge pull request #128 from EverythingMe/fix_visualizations_issues
Several visualizations related fixes.
2014-03-06 22:09:43 +02:00
Arik Fraimovich
02f376b6d3 Hackish way to show dates as dates in the table and timestamps
as date+timestamp.
2014-03-06 19:58:04 +02:00
Arik Fraimovich
db1df07337 Hackish way to show dates as dates in the table and timestamps
as date+timestamp.
2014-03-06 19:58:04 +02:00
Arik Fraimovich
10f2bc3df5 Fix: custom visualization name was ignored (ref #127) 2014-03-06 19:23:56 +02:00
Arik Fraimovich
ceb2e0cfb3 Fix: custom visualization name was ignored (ref #127) 2014-03-06 19:23:56 +02:00
Arik Fraimovich
3e7b1cdc15 Pie chart: show value and not only % 2014-03-06 19:21:13 +02:00
Arik Fraimovich
5e981a579b Pie chart: show value and not only % 2014-03-06 19:21:13 +02:00
Arik Fraimovich
234b15765c Fix: the pivot table visualization was messing up other visualizations
by changing the data.
2014-03-06 19:18:07 +02:00
Arik Fraimovich
2b03973cf0 Fix: the pivot table visualization was messing up other visualizations
by changing the data.
2014-03-06 19:18:07 +02:00
Alessio Garofalo
53d81aebed Add gunicorn to the requirements
So when we install it is already there.
2014-03-06 12:17:02 +00:00
Arik Fraimovich
462aaad9c0 Merge pull request #124 from erans/master
BigQuery support
2014-03-05 14:40:28 +02:00
Arik Fraimovich
afac41d3e6 Merge pull request #124 from erans/master
BigQuery support
2014-03-05 14:40:28 +02:00
Eran Sandler
4f72a61ea6 Added try..except to handle missing imports 2014-03-05 09:05:33 +02:00
Eran Sandler
f54d08a628 Added try..except to handle missing imports 2014-03-05 09:05:33 +02:00
Eran Sandler
bc1ae8b496 Bigquery support 2014-03-05 08:46:27 +02:00
Eran Sandler
5b42a4b36e Bigquery support 2014-03-05 08:46:27 +02:00
Eran Sandler
98ee88c1bb No need to use github style authenticated URLs, it can just break things. 2014-03-05 08:46:09 +02:00
Eran Sandler
7c89ff5c1b No need to use github style authenticated URLs, it can just break things. 2014-03-05 08:46:09 +02:00
Arik Fraimovich
bd8abbbdbd Merge pull request #122 from hailocab/DAT-706
Adding a new table called activity_log to log who runs what and when
2014-03-04 17:41:08 +02:00
Arik Fraimovich
9249dfee4c Merge pull request #122 from hailocab/DAT-706
Adding a new table called activity_log to log who runs what and when
2014-03-04 17:41:08 +02:00
Christopher Valles
1ac945ad66 Adding a constant to activity model 2014-03-04 14:36:57 +00:00
Christopher Valles
e270d2534f Adding a constant to activity model 2014-03-04 14:36:57 +00:00
Arik Fraimovich
c2b038c1c0 Merge pull request #123 from ekampf/feature/fixmyql
Handle empty data returned by queries
2014-03-04 15:36:00 +02:00
Arik Fraimovich
d5862f476b Merge pull request #123 from ekampf/feature/fixmyql
Handle empty data returned by queries
2014-03-04 15:36:00 +02:00
Eran Kampf
02b5179eb3 Handle empty data returned by queries 2014-03-04 15:21:03 +02:00
Eran Kampf
100fd2c9f0 Handle empty data returned by queries 2014-03-04 15:21:03 +02:00
Christopher Valles
a2f55b9838 Removing unneeded imports in migration script 2014-03-04 13:12:43 +00:00
Christopher Valles
4fef4a8d33 Removing unneeded imports in migration script 2014-03-04 13:12:43 +00:00
Christopher Valles
933f799952 Merge branch 'master' into DAT-706 2014-03-04 13:10:34 +00:00
Christopher Valles
3018f8c521 Merge branch 'master' into DAT-706 2014-03-04 13:10:34 +00:00
Christopher Valles
826fccbc94 Adding a new table called activity_log to log who runs what and when 2014-03-04 13:04:55 +00:00
Christopher Valles
54453ee9e5 Adding a new table called activity_log to log who runs what and when 2014-03-04 13:04:55 +00:00
Amir Nissim
be0b5bb0d1 Merge pull request #100 from EverythingMe/feature_visualization_options
Visualizations refactor
2014-03-04 11:42:57 +02:00
Amir Nissim
cc957cc3e8 Merge pull request #100 from EverythingMe/feature_visualization_options
Visualizations refactor
2014-03-04 11:42:57 +02:00
Arik Fraimovich
2b274b706e Set default name when creating 2014-03-04 11:38:00 +02:00
Arik Fraimovich
dd5fd72bd2 Set default name when creating 2014-03-04 11:38:00 +02:00
Arik Fraimovich
3ab1f9b5a3 Fixes #81: reset query when saving widget 2014-03-04 11:33:11 +02:00
Arik Fraimovich
9d4655cc00 Fixes #81: reset query when saving widget 2014-03-04 11:33:11 +02:00
Arik Fraimovich
e512fef78c Switch to config object instead of millions of params 2014-03-04 11:09:04 +02:00
Arik Fraimovich
3320de07f2 Switch to config object instead of millions of params 2014-03-04 11:09:04 +02:00
Arik Fraimovich
448e82108d Fix: reset visualization form after saving visualization 2014-03-04 10:59:07 +02:00
Arik Fraimovich
68482afa5c Fix: reset visualization form after saving visualization 2014-03-04 10:59:07 +02:00
Arik Fraimovich
be93e77b2f Rename Visaulization to VisualizationProvider 2014-03-04 10:19:28 +02:00
Arik Fraimovich
bfeded207a Rename Visaulization to VisualizationProvider 2014-03-04 10:19:28 +02:00
Arik Fraimovich
5aed2b6baf typo fix 2014-03-04 10:17:09 +02:00
Arik Fraimovich
a5971b0c69 typo fix 2014-03-04 10:17:09 +02:00
Arik Fraimovich
00b5aba88a Remove the need to declare each visualization module in app dependencies. 2014-03-04 10:17:08 +02:00
Arik Fraimovich
6d93ccc0d0 Remove the need to declare each visualization module in app dependencies. 2014-03-04 10:17:08 +02:00
Arik Fraimovich
9c0edfdb9d Fix chart editor declaration. 2014-03-04 10:17:08 +02:00
Arik Fraimovich
69f5de6478 Fix chart editor declaration. 2014-03-04 10:17:08 +02:00
Arik Fraimovich
b40e2e0a6f Fix switch regular expression. 2014-03-04 10:17:08 +02:00
Arik Fraimovich
4630a8d18d Fix switch regular expression. 2014-03-04 10:17:08 +02:00
Arik Fraimovich
d73130ebac Refactor visualizations:
The main code doesn't know about individual visualizations and each visualization is contained in its own module. Should make adding/editing/removing visualizations easier.
2014-03-04 10:17:08 +02:00
Arik Fraimovich
79e40a667b Refactor visualizations:
The main code doesn't know about individual visualizations and each visualization is contained in its own module. Should make adding/editing/removing visualizations easier.
2014-03-04 10:17:08 +02:00
Arik Fraimovich
13016c7476 Remove unused dependency. 2014-03-04 10:16:32 +02:00
Arik Fraimovich
2c904641a5 Remove unused dependency. 2014-03-04 10:16:32 +02:00
Arik Fraimovich
667eb3035b Merge pull request #119 from EverythingMe/fix_chart_name
Set the chart name by default to chart type.
2014-03-03 20:47:21 +02:00
Arik Fraimovich
1303163aee Merge pull request #119 from EverythingMe/fix_chart_name
Set the chart name by default to chart type.
2014-03-03 20:47:21 +02:00
Arik Fraimovich
13f2ee2ae8 Fix: set the chart name by default to chart type. 2014-03-03 20:40:50 +02:00
Arik Fraimovich
14ecfd2cc8 Fix: set the chart name by default to chart type. 2014-03-03 20:40:50 +02:00
Arik Fraimovich
1b46c39a27 typo fix 2014-03-03 20:29:43 +02:00
Arik Fraimovich
a91eb9435b typo fix 2014-03-03 20:29:43 +02:00
Arik Fraimovich
5d19096e0c merge conflict fix & bump version 2014-03-03 20:27:04 +02:00
Arik Fraimovich
b5d2285b99 merge conflict fix & bump version 2014-03-03 20:27:04 +02:00
Arik Fraimovich
3f79189410 Merge pull request #118 from EverythingMe/feature_statsd
Feature: StatsD integration
2014-03-03 20:25:05 +02:00
Arik Fraimovich
fece24a50a Merge pull request #118 from EverythingMe/feature_statsd
Feature: StatsD integration
2014-03-03 20:25:05 +02:00
Arik Fraimovich
1940099d3c Merge pull request #112 from hailocab/DAT-741
PR to fix Issue 82
2014-03-03 20:17:54 +02:00
Arik Fraimovich
7d77da8339 Merge pull request #112 from hailocab/DAT-741
PR to fix Issue 82
2014-03-03 20:17:54 +02:00
Arik Fraimovich
240e0780a0 Basic stats reporting. 2014-03-03 20:17:25 +02:00
Arik Fraimovich
e43366f422 Basic stats reporting. 2014-03-03 20:17:25 +02:00
Arik Fraimovich
3e38ef959b Use integers instead of uuid for workers id. 2014-03-03 20:16:42 +02:00
Arik Fraimovich
c7af5bdce9 Use integers instead of uuid for workers id. 2014-03-03 20:16:42 +02:00
Arik Fraimovich
9e2af21d5e Statsd settings. 2014-03-03 20:15:14 +02:00
Arik Fraimovich
3f302ee4a3 Statsd settings. 2014-03-03 20:15:14 +02:00
Arik Fraimovich
3aa4d4c36c Add statsd client requirements. 2014-03-03 20:15:02 +02:00
Arik Fraimovich
53ef0f3f2d Add statsd client requirements. 2014-03-03 20:15:02 +02:00
Christopher Valles
81866cb6d3 Resolve conflicts 2014-03-03 17:35:01 +00:00
Christopher Valles
c6dbb8d7c8 Resolve conflicts 2014-03-03 17:35:01 +00:00
Christopher Valles
bee20a5478 Merge remote-tracking branch 'upstream/master' 2014-03-03 17:33:40 +00:00
Christopher Valles
f4088e0b38 Merge remote-tracking branch 'upstream/master' 2014-03-03 17:33:40 +00:00
Christopher Valles
b43e32169b Merge branch 'master' into DAT-741 2014-03-03 17:18:15 +00:00
Christopher Valles
d3d46aa023 Merge branch 'master' into DAT-741 2014-03-03 17:18:15 +00:00
Christopher Valles
4d99541f7c Fixing PR #112 as discussed with Arik 2014-03-03 17:11:38 +00:00
Christopher Valles
55cc3dd90e Fixing PR #112 as discussed with Arik 2014-03-03 17:11:38 +00:00
Christopher Valles
089b67c40e Fixing PR #112 as discussed with Arik 2014-03-03 17:08:07 +00:00
Christopher Valles
0822789002 Fixing PR #112 as discussed with Arik 2014-03-03 17:08:07 +00:00
Christopher Valles
9ca0f4a4fa Fixing PR #112 as discussed with Arik 2014-03-03 16:45:45 +00:00
Christopher Valles
ffb2ec9bd1 Fixing PR #112 as discussed with Arik 2014-03-03 16:45:45 +00:00
Christopher Valles
0e1a0b4798 Fixing PR #112 as discussed with Arik 2014-03-03 16:41:53 +00:00
Christopher Valles
2bcb56d249 Fixing PR #112 as discussed with Arik 2014-03-03 16:41:53 +00:00
Arik Fraimovich
467ae5c8fa Update the refresh queries query 2014-03-03 18:26:15 +02:00
Arik Fraimovich
8ccbe9c069 Update the refresh queries query 2014-03-03 18:26:15 +02:00
Arik Fraimovich
a3bf50e15e Merge pull request #116 from EverythingMe/fix_category_graphs
Fixes to category charts
2014-03-03 15:31:30 +02:00
Arik Fraimovich
85f98f7405 Merge pull request #116 from EverythingMe/fix_category_graphs
Fixes to category charts
2014-03-03 15:31:30 +02:00
Arik Fraimovich
9d44a73d02 Feature: sort category charts by y value. 2014-03-03 15:27:39 +02:00
Arik Fraimovich
ac946fd014 Feature: sort category charts by y value. 2014-03-03 15:27:39 +02:00
Arik Fraimovich
8e9d537882 Fix: graphs with category as x axis were shown as datetime
graphs, because drawChart is called twice and on second pass
there is no x attribute on point object.
2014-03-03 15:21:49 +02:00
Arik Fraimovich
3680d0c65d Fix: graphs with category as x axis were shown as datetime
graphs, because drawChart is called twice and on second pass
there is no x attribute on point object.
2014-03-03 15:21:49 +02:00
Christopher Valles
774b9cc368 Merge remote-tracking branch 'upstream/master' 2014-03-03 11:52:55 +00:00
Christopher Valles
8130d28442 Merge remote-tracking branch 'upstream/master' 2014-03-03 11:52:55 +00:00
Arik Fraimovich
00e3b06004 Merge pull request #114 from EverythingMe/feature_login_form
Feature: non OpenID users & login screen
2014-03-03 13:10:40 +02:00
Arik Fraimovich
9cac38d5da Merge pull request #114 from EverythingMe/feature_login_form
Feature: non OpenID users & login screen
2014-03-03 13:10:40 +02:00
Arik Fraimovich
3014ba8eec Fix: create_user_and_login should accept user 2014-03-03 13:07:57 +02:00
Arik Fraimovich
81122c9865 Fix: create_user_and_login should accept user 2014-03-03 13:07:57 +02:00
Arik Fraimovich
823f0b8db5 user management commands 2014-03-03 12:18:15 +02:00
Arik Fraimovich
b8a0077b1d user management commands 2014-03-03 12:18:15 +02:00
Arik Fraimovich
af1b1c0edb Set is_admin of user based on ADMINS list. 2014-03-03 11:53:49 +02:00
Arik Fraimovich
62108e3dac Set is_admin of user based on ADMINS list. 2014-03-03 11:53:49 +02:00
Arik Fraimovich
dd4c3f152a Build assets for login page 2014-03-03 11:49:31 +02:00
Arik Fraimovich
0c9fa8b51b Build assets for login page 2014-03-03 11:49:31 +02:00
Arik Fraimovich
0a511e4f8a Ability to disable openid or password login 2014-03-02 21:54:50 +02:00
Arik Fraimovich
aa2bf4fe22 Ability to disable openid or password login 2014-03-02 21:54:50 +02:00
Arik Fraimovich
524c2b8203 BaseResource.current_user wrapper to get real user object. 2014-03-02 18:30:06 +02:00
Arik Fraimovich
e82f561c03 BaseResource.current_user wrapper to get real user object. 2014-03-02 18:30:06 +02:00
Arik Fraimovich
578d9c6785 Logout controller 2014-03-02 18:27:05 +02:00
Arik Fraimovich
d348fe9012 Logout controller 2014-03-02 18:27:05 +02:00
Arik Fraimovich
c7efad2197 Login view 2014-03-02 17:59:08 +02:00
Arik Fraimovich
7271b7a5f0 Login view 2014-03-02 17:59:08 +02:00
Arik Fraimovich
adda8707ba CircleCi: install dev_requirements.txt 2014-03-02 15:46:29 +02:00
Arik Fraimovich
522536cfe0 CircleCi: install dev_requirements.txt 2014-03-02 15:46:29 +02:00
Arik Fraimovich
640d0082da Tests for authentication functions 2014-03-02 15:41:38 +02:00
Arik Fraimovich
f557b53ce2 Tests for authentication functions 2014-03-02 15:41:38 +02:00
Arik Fraimovich
f5bd7f113f Chagne logging not to depend on app context 2014-03-02 15:41:20 +02:00
Arik Fraimovich
1277da7e92 Chagne logging not to depend on app context 2014-03-02 15:41:20 +02:00
Arik Fraimovich
8b1978fb26 Add mock to dev_requirements 2014-03-02 15:37:33 +02:00
Arik Fraimovich
f334122e41 Add mock to dev_requirements 2014-03-02 15:37:33 +02:00
Arik Fraimovich
812e8cca9a Add flask_login and use it for managing authentication 2014-03-02 14:42:13 +02:00
Arik Fraimovich
269cbe839b Add flask_login and use it for managing authentication 2014-03-02 14:42:13 +02:00
Arik Fraimovich
63bc04e800 Bump version. 2014-02-27 12:55:06 +02:00
Arik Fraimovich
2a3bcc2ecb Bump version. 2014-02-27 12:55:06 +02:00
Arik Fraimovich
7eb776bc3f Remove milestone v0.2 from README. 2014-02-27 12:45:56 +02:00
Arik Fraimovich
5babab85c8 Remove milestone v0.2 from README. 2014-02-27 12:45:56 +02:00
Arik Fraimovich
56981a5333 Merge pull request #105 from EverythingMe/feature_user_object
Resolve #17: User model
2014-02-27 12:44:30 +02:00
Arik Fraimovich
8debd01a36 Merge pull request #105 from EverythingMe/feature_user_object
Resolve #17: User model
2014-02-27 12:44:30 +02:00
Arik Fraimovich
54cd4723ba Fix: saving new query. 2014-02-27 10:24:28 +02:00
Arik Fraimovich
51a37cae3d Fix: saving new query. 2014-02-27 10:24:28 +02:00
Christopher Valles
c9f8b04a12 UX/UI issues with visualizations fixed 2014-02-25 19:59:14 +00:00
Christopher Valles
3c24e76eb4 UX/UI issues with visualizations fixed 2014-02-25 19:59:14 +00:00
Christopher Valles
11e970ee8a Merge branch 'master' into DAT-741 2014-02-25 18:15:48 +00:00
Christopher Valles
6dc9f8ea2b Merge branch 'master' into DAT-741 2014-02-25 18:15:48 +00:00
Christopher Valles
3d7367aa04 Merge remote-tracking branch 'upstream/master' 2014-02-25 18:15:24 +00:00
Christopher Valles
157b1ca0b4 Merge remote-tracking branch 'upstream/master' 2014-02-25 18:15:24 +00:00
Christopher Valles
2bcf5b2fc5 DAT-741 2014-02-25 18:14:47 +00:00
Christopher Valles
8be95262d4 DAT-741 2014-02-25 18:14:47 +00:00
Arik Fraimovich
39bc4d7151 Merge pull request #111 from EverythingMe/fix_graphite_settings
Fix: added JSON parsing of the Graphite settings
2014-02-25 08:38:32 +02:00
Arik Fraimovich
3cbdae6e5c Merge pull request #111 from EverythingMe/fix_graphite_settings
Fix: added JSON parsing of the Graphite settings
2014-02-25 08:38:32 +02:00
Arik Fraimovich
f08e58a301 Fix: add parsing of graphite settings 2014-02-25 08:37:19 +02:00
Arik Fraimovich
edcf0661a6 Fix: add parsing of graphite settings 2014-02-25 08:37:19 +02:00
Arik Fraimovich
a49270630c Fix graphite settings example 2014-02-25 08:36:59 +02:00
Arik Fraimovich
6d14c5c555 Fix graphite settings example 2014-02-25 08:36:59 +02:00
Arik Fraimovich
f703517f70 Remove outdated vagrant file 2014-02-25 08:17:16 +02:00
Arik Fraimovich
a0662d5323 Remove outdated vagrant file 2014-02-25 08:17:16 +02:00
Arik Fraimovich
6c1ca3036b Make sure visualization don't overflow 2014-02-25 08:16:36 +02:00
Arik Fraimovich
cbd1cf7c25 Make sure visualization don't overflow 2014-02-25 08:16:36 +02:00
Arik Fraimovich
6ed80a9b92 Merge pull request #110 from ekampf/feature/fixmyql
Fixed mysql error handling
2014-02-24 20:20:20 +02:00
Arik Fraimovich
a55225b5e8 Merge pull request #110 from ekampf/feature/fixmyql
Fixed mysql error handling
2014-02-24 20:20:20 +02:00
Eran Kampf
42fa5c2ee7 Fixed MySQL Errors 2014-02-24 16:44:08 +02:00
Eran Kampf
b81c3ba614 Fixed MySQL Errors 2014-02-24 16:44:08 +02:00
Arik Fraimovich
8f34b241d4 Update Getting Started instructions. 2014-02-24 14:40:47 +02:00
Arik Fraimovich
2d0998a995 Update Getting Started instructions. 2014-02-24 14:40:47 +02:00
Arik Fraimovich
b0d6ce61b0 Fix tests 2014-02-22 14:52:04 +02:00
Arik Fraimovich
766840de68 Fix tests 2014-02-22 14:52:04 +02:00
Arik Fraimovich
9defa45428 Use of user object (fix views, update migrations and some). 2014-02-22 14:43:00 +02:00
Arik Fraimovich
791f2e0b34 Use of user object (fix views, update migrations and some). 2014-02-22 14:43:00 +02:00
Arik Fraimovich
52bcb8dfb6 User model & migration (ref #17) 2014-02-18 11:15:46 +02:00
Arik Fraimovich
9241a7c35d User model & migration (ref #17) 2014-02-18 11:15:46 +02:00
Arik Fraimovich
1f90f13b81 Merge pull request #103 from EverythingMe/refresh_button
Use database number from redis url if available.
2014-02-17 18:02:22 +02:00
Arik Fraimovich
dda92477cf Merge pull request #103 from EverythingMe/refresh_button
Use database number from redis url if available.
2014-02-17 18:02:22 +02:00
Arik Fraimovich
0a522863dc Use database number from redis url if available. 2014-02-17 18:01:44 +02:00
Arik Fraimovich
07455e5821 Use database number from redis url if available. 2014-02-17 18:01:44 +02:00
Arik Fraimovich
e8a974813d Merge pull request #102 from EverythingMe/refresh_button
Only refresh widgets that have their query data updated.
2014-02-17 17:59:27 +02:00
Arik Fraimovich
1b9aae0137 Merge pull request #102 from EverythingMe/refresh_button
Only refresh widgets that have their query data updated.
2014-02-17 17:59:27 +02:00
Arik Fraimovich
50da387936 Only refresh widgets that have their query data updated. 2014-02-17 17:57:26 +02:00
Arik Fraimovich
30b86ea781 Only refresh widgets that have their query data updated. 2014-02-17 17:57:26 +02:00
Arik Fraimovich
489869ee42 Merge pull request #101 from EverythingMe/refresh_button
Auto-refresh button for dashboards & every minute refresh rate
2014-02-17 17:22:22 +02:00
Arik Fraimovich
a186d44d8f Merge pull request #101 from EverythingMe/refresh_button
Auto-refresh button for dashboards & every minute refresh rate
2014-02-17 17:22:22 +02:00
Arik Fraimovich
316b2a1b1c Option to set every minute refresh rate. 2014-02-17 17:19:48 +02:00
Arik Fraimovich
574f75b293 Option to set every minute refresh rate. 2014-02-17 17:19:48 +02:00
Arik Fraimovich
a1625f7125 Auto-refresh button for dashboards. 2014-02-17 17:19:32 +02:00
Arik Fraimovich
252ae7455a Auto-refresh button for dashboards. 2014-02-17 17:19:32 +02:00
Christopher Valles
63379d9b24 Adding .ruby-version to .gitignore 2014-02-14 11:57:42 +00:00
Christopher Valles
d73dbdeee0 Adding .ruby-version to .gitignore 2014-02-14 11:57:42 +00:00
Arik Fraimovich
d812f26e81 Merge pull request #99 from EverythingMe/feature_allow_external_users
Procfile changes:
2014-02-13 20:21:08 +02:00
Arik Fraimovich
72065c0ee2 Merge pull request #99 from EverythingMe/feature_allow_external_users
Procfile changes:
2014-02-13 20:21:08 +02:00
Arik Fraimovich
4ba3152a99 Procfile changes:
1. Renamed Honchofile -> Procfile.heroku and changed it to work better with Heroku.
2. Added Procfile.dev for development.
2014-02-13 20:16:36 +02:00
Arik Fraimovich
07caee1d12 Procfile changes:
1. Renamed Honchofile -> Procfile.heroku and changed it to work better with Heroku.
2. Added Procfile.dev for development.
2014-02-13 20:16:36 +02:00
Arik Fraimovich
d4f48cdc21 Merge pull request #98 from EverythingMe/feature_allow_external_users
Feature: allow external users
2014-02-13 20:15:54 +02:00
Arik Fraimovich
4c3904760c Merge pull request #98 from EverythingMe/feature_allow_external_users
Feature: allow external users
2014-02-13 20:15:54 +02:00
Arik Fraimovich
dc0cc3af65 If only domain specified and not external users, use federated login. 2014-02-13 20:13:08 +02:00
Arik Fraimovich
8ad2c2a59e If only domain specified and not external users, use federated login. 2014-02-13 20:13:08 +02:00
Arik Fraimovich
27031c96b5 Bring back the ability to set allowed external users & publicly open re:dash. 2014-02-13 20:04:28 +02:00
Arik Fraimovich
e5a365ba41 Bring back the ability to set allowed external users & publicly open re:dash. 2014-02-13 20:04:28 +02:00
Arik Fraimovich
b1ca28fbb5 Merge pull request #96 from EverythingMe/fix_description_nullable
Fix: allow queries.description to be null (+ migration)
2014-02-13 19:18:39 +02:00
Arik Fraimovich
fc0b118188 Merge pull request #96 from EverythingMe/fix_description_nullable
Fix: allow queries.description to be null (+ migration)
2014-02-13 19:18:39 +02:00
Arik Fraimovich
1b7bfb42fc Fix: allow queries.description to be null. 2014-02-13 19:08:35 +02:00
Arik Fraimovich
a207b93d0d Fix: allow queries.description to be null. 2014-02-13 19:08:35 +02:00
Arik Fraimovich
ea65204eaa Merge pull request #95 from EverythingMe/feature_stacking_selection
Allow user to set the stacking of the chart.
2014-02-13 16:24:39 +02:00
Arik Fraimovich
b1d588b1f2 Merge pull request #95 from EverythingMe/feature_stacking_selection
Allow user to set the stacking of the chart.
2014-02-13 16:24:39 +02:00
Arik Fraimovich
4351e5a642 Allow user to set the stacking of the chart. 2014-02-13 16:19:15 +02:00
Arik Fraimovich
95a6bab8b5 Allow user to set the stacking of the chart. 2014-02-13 16:19:15 +02:00
Arik Fraimovich
f35289624c CirlceCI: no longer need to delete settings.py. 2014-02-13 14:50:42 +02:00
Arik Fraimovich
c82433e6b4 CirlceCI: no longer need to delete settings.py. 2014-02-13 14:50:42 +02:00
Arik Fraimovich
47c322cb31 Merge pull request #94 from EverythingMe/fix_query_hash_not_updating
Fix: when updating query text the hash should change.
2014-02-13 13:13:38 +02:00
Arik Fraimovich
2e84852519 Merge pull request #94 from EverythingMe/fix_query_hash_not_updating
Fix: when updating query text the hash should change.
2014-02-13 13:13:38 +02:00
Arik Fraimovich
88f1237990 Fix: when updating query text the hash should change. 2014-02-13 13:08:48 +02:00
Arik Fraimovich
da746d15a0 Fix: when updating query text the hash should change. 2014-02-13 13:08:48 +02:00
Arik Fraimovich
4740a8b520 Merge pull request #93 from EverythingMe/feature_env
Feature: better Heroku support - move configuration to environment variables & Procfile
2014-02-13 12:15:52 +02:00
Arik Fraimovich
1b519269d8 Merge pull request #93 from EverythingMe/feature_env
Feature: better Heroku support - move configuration to environment variables & Procfile
2014-02-13 12:15:52 +02:00
Arik Fraimovich
521b6ab851 Fix CircleCI configuration 2014-02-12 21:37:56 +02:00
Arik Fraimovich
5ffaf1aead Fix CircleCI configuration 2014-02-12 21:37:56 +02:00
Arik Fraimovich
9e328551e4 Example .env file. 2014-02-12 20:53:32 +02:00
Arik Fraimovich
b704406164 Example .env file. 2014-02-12 20:53:32 +02:00
Arik Fraimovich
44eaffd110 Bump version. 2014-02-12 20:52:36 +02:00
Arik Fraimovich
5c9fe40702 Bump version. 2014-02-12 20:52:36 +02:00
Arik Fraimovich
cb964b5888 Fix: allow passing relative path for assets. 2014-02-12 20:52:19 +02:00
Arik Fraimovich
fe7c4f96aa Fix: allow passing relative path for assets. 2014-02-12 20:52:19 +02:00
Arik Fraimovich
81cbc7b87c Read settings from environment variables instead of a settings file.
This is mostly done to make it easier to run re:dash on Heroku but should be convenient in other platforms too.
2014-02-12 20:43:41 +02:00
Arik Fraimovich
83909a07fa Read settings from environment variables instead of a settings file.
This is mostly done to make it easier to run re:dash on Heroku but should be convenient in other platforms too.
2014-02-12 20:43:41 +02:00
Arik Fraimovich
8fa45749a9 Add Honcho (foreman alternative in Python) file(s).
The reason we have both Procfile and Honchofile is to be able to run both the workers and the web server in a single dyno on Heroku.
2014-02-12 20:42:32 +02:00
Arik Fraimovich
cd99927881 Add Honcho (foreman alternative in Python) file(s).
The reason we have both Procfile and Honchofile is to be able to run both the workers and the web server in a single dyno on Heroku.
2014-02-12 20:42:32 +02:00
Arik Fraimovich
910ea4caec Rename test files to test_. 2014-02-12 20:41:36 +02:00
Arik Fraimovich
8bbb485d5b Rename test files to test_. 2014-02-12 20:41:36 +02:00
Arik Fraimovich
0bff263c4b Merge pull request #89 from EverythingMe/feature_pie_chart
Feature: pie charts
2014-02-11 16:46:12 +02:00
Arik Fraimovich
b2ec77668e Merge pull request #89 from EverythingMe/feature_pie_chart
Feature: pie charts
2014-02-11 16:46:12 +02:00
Arik Fraimovich
38f85d3cc8 Better support for single series tooltips. 2014-02-11 16:30:41 +02:00
Arik Fraimovich
f8302ab65a Better support for single series tooltips. 2014-02-11 16:30:41 +02:00
Arik Fraimovich
83002d09a4 Support for pie charts. 2014-02-11 16:30:23 +02:00
Arik Fraimovich
e632cf1c42 Support for pie charts. 2014-02-11 16:30:23 +02:00
Arik Fraimovich
a567178987 Merge pull request #88 from EverythingMe/feature_graphite_v2
Feature: graphite query runner
2014-02-11 11:47:11 +02:00
Arik Fraimovich
640557df4f Merge pull request #88 from EverythingMe/feature_graphite_v2
Feature: graphite query runner
2014-02-11 11:47:11 +02:00
Arik Fraimovich
13c47639da Make the default newOptions apply to all but the chart vis 2014-02-11 11:42:37 +02:00
Arik Fraimovich
9b7227a88b Make the default newOptions apply to all but the chart vis 2014-02-11 11:42:37 +02:00
Arik Fraimovich
74b0535b31 Graphite query runner support 2014-02-11 11:38:34 +02:00
Arik Fraimovich
aabc912862 Graphite query runner support 2014-02-11 11:38:34 +02:00
Arik Fraimovich
cbd7799b44 Imrpove (?) line chart settings 2014-02-11 11:38:01 +02:00
Arik Fraimovich
02d6567347 Imrpove (?) line chart settings 2014-02-11 11:38:01 +02:00
Arik Fraimovich
98a8c4752b Merge pull request #87 from EverythingMe/fix_viz
Some more visualizations UI updates
2014-02-10 21:11:13 +02:00
Arik Fraimovich
6f8767d1fc Merge pull request #87 from EverythingMe/fix_viz
Some more visualizations UI updates
2014-02-10 21:11:13 +02:00
Arik Fraimovich
b2debb32d1 Show delete/edit/create new visualization only to query owner.
This is a temporary solution until we have owners for visualizations.
2014-02-10 21:06:52 +02:00
Arik Fraimovich
bc787efc86 Show delete/edit/create new visualization only to query owner.
This is a temporary solution until we have owners for visualizations.
2014-02-10 21:06:52 +02:00
Arik Fraimovich
098f3f6e4c When clicking on widget in dashboard, it should take to the correct visualization tab. 2014-02-10 19:38:23 +02:00
Arik Fraimovich
e0d46c3942 When clicking on widget in dashboard, it should take to the correct visualization tab. 2014-02-10 19:38:23 +02:00
Arik Fraimovich
e8c7f728a2 Merge pull request #86 from EverythingMe/fix_viz
Fixes and improvements (most related to visualizations)
2014-02-10 10:29:40 +02:00
Arik Fraimovich
5a2bed29aa Merge pull request #86 from EverythingMe/fix_viz
Fixes and improvements (most related to visualizations)
2014-02-10 10:29:40 +02:00
Arik Fraimovich
387ffbb0fc Performance improvements for chart rendering:
1. Don't redraw when adding or removing a single series, but redraw at the end.
2. Use $timeout to postpone high charts rendering until DOM is ready.
2014-02-10 10:05:56 +02:00
Arik Fraimovich
8fbcd0c34d Performance improvements for chart rendering:
1. Don't redraw when adding or removing a single series, but redraw at the end.
2. Use $timeout to postpone high charts rendering until DOM is ready.
2014-02-10 10:05:56 +02:00
Arik Fraimovich
d2d4f6186f Remove SERIES_TYPES from Visualization. 2014-02-10 09:55:49 +02:00
Arik Fraimovich
97df37536c Remove SERIES_TYPES from Visualization. 2014-02-10 09:55:49 +02:00
Arik Fraimovich
d5cd02cab3 Bring back logging level setting 2014-02-09 21:03:24 +02:00
Arik Fraimovich
373b9c6a97 Bring back logging level setting 2014-02-09 21:03:24 +02:00
Arik Fraimovich
d831710b0a Fix for high charts bug with stacked areas. 2014-02-09 20:42:01 +02:00
Arik Fraimovich
009726c62d Fix for high charts bug with stacked areas. 2014-02-09 20:42:01 +02:00
Arik Fraimovich
d5316b2c4d Make tooltip work for all chart types. 2014-02-09 20:28:37 +02:00
Arik Fraimovich
69c07a41e9 Make tooltip work for all chart types. 2014-02-09 20:28:37 +02:00
Arik Fraimovich
7c4bedf371 Add scatter plot type.
cc: @christophervalles
2014-02-09 20:17:29 +02:00
Arik Fraimovich
64afd62a1f Add scatter plot type.
cc: @christophervalles
2014-02-09 20:17:29 +02:00
Arik Fraimovich
7018ed28fb There is no bar chart type -- it's column. 2014-02-09 20:03:32 +02:00
Arik Fraimovich
4318468957 There is no bar chart type -- it's column. 2014-02-09 20:03:32 +02:00
Arik Fraimovich
7213e62937 After duplicating a query, put user back on table tab. 2014-02-09 20:02:58 +02:00
Arik Fraimovich
1af3fc1c96 After duplicating a query, put user back on table tab. 2014-02-09 20:02:58 +02:00
Arik Fraimovich
219ea98f33 Set description of default table visualization to "". 2014-02-09 20:02:38 +02:00
Arik Fraimovich
1e11f8032a Set description of default table visualization to "". 2014-02-09 20:02:38 +02:00
Arik Fraimovich
f6cbc36112 Set Visualization.description to nullable. 2014-02-09 19:38:41 +02:00
Arik Fraimovich
a1a7ca8a0a Set Visualization.description to nullable. 2014-02-09 19:38:41 +02:00
Arik Fraimovich
93bc54e275 Return query with visualizations when saving. 2014-02-09 19:38:24 +02:00
Arik Fraimovich
52758fa66e Return query with visualizations when saving. 2014-02-09 19:38:24 +02:00
Arik Fraimovich
44cd109ba3 Set default visualization description to ''. 2014-02-09 19:34:43 +02:00
Arik Fraimovich
fa43ff1365 Set default visualization description to ''. 2014-02-09 19:34:43 +02:00
Arik Fraimovich
482168f98e Merge pull request #84 from EverythingMe/refactor_flask
Big refactor: flask, peewee, tests, structure changes and more
2014-02-09 19:11:59 +02:00
Arik Fraimovich
bd15162fb7 Merge pull request #84 from EverythingMe/refactor_flask
Big refactor: flask, peewee, tests, structure changes and more
2014-02-09 19:11:59 +02:00
Arik Fraimovich
f9b9c7136e Remove coveralls.io integration as it's breaking builds. 2014-02-09 19:07:56 +02:00
Arik Fraimovich
cc980edc66 Remove coveralls.io integration as it's breaking builds. 2014-02-09 19:07:56 +02:00
Arik Fraimovich
84ec26f648 Tests for HMAC authentication. 2014-02-09 18:51:04 +02:00
Arik Fraimovich
7fd094ba39 Tests for HMAC authentication. 2014-02-09 18:51:04 +02:00
Arik Fraimovich
fcfe5da506 Add dev_requirements.txt file. 2014-02-09 17:37:47 +02:00
Arik Fraimovich
68ef489d8c Add dev_requirements.txt file. 2014-02-09 17:37:47 +02:00
Arik Fraimovich
1e4bdb367e Change coveralls badge to point at master branch 2014-02-09 17:37:37 +02:00
Arik Fraimovich
21ff1d7482 Change coveralls badge to point at master branch 2014-02-09 17:37:37 +02:00
Arik Fraimovich
d3ee55a971 Switch to Flask-Script. 2014-02-09 17:09:07 +02:00
Arik Fraimovich
669b1d9a63 Switch to Flask-Script. 2014-02-09 17:09:07 +02:00
Arik Fraimovich
3a967c5985 Move version information into python package. 2014-02-09 16:46:32 +02:00
Arik Fraimovich
29531a361c Move version information into python package. 2014-02-09 16:46:32 +02:00
Arik Fraimovich
92f5df4704 Improve visualizations migration 2014-02-09 16:40:39 +02:00
Arik Fraimovich
c40cf2e7e8 Improve visualizations migration 2014-02-09 16:40:39 +02:00
Arik Fraimovich
2e8789de3b Set automatic releases as 'prerelease'. 2014-02-09 15:20:58 +02:00
Arik Fraimovich
7bf391e772 Set automatic releases as 'prerelease'. 2014-02-09 15:20:58 +02:00
Arik Fraimovich
b7827f3eea Update visualizations migration. 2014-02-09 15:14:46 +02:00
Arik Fraimovich
fbb84af955 Update visualizations migration. 2014-02-09 15:14:46 +02:00
Arik Fraimovich
8c101a1bbf Update getting started instructions in the README. 2014-02-09 15:00:42 +02:00
Arik Fraimovich
d954eb63ef Update getting started instructions in the README. 2014-02-09 15:00:42 +02:00
Arik Fraimovich
ee216dbf64 Show query name in dashboard editor 2014-02-09 14:48:15 +02:00
Arik Fraimovich
1b14161535 Show query name in dashboard editor 2014-02-09 14:48:15 +02:00
Arik Fraimovich
54675117de Fix: bring back TABLE renderer to VisualizationRenderer. 2014-02-09 14:37:48 +02:00
Arik Fraimovich
bcf854604b Fix: bring back TABLE renderer to VisualizationRenderer. 2014-02-09 14:37:48 +02:00
Arik Fraimovich
30d5b46daf Fix: POST api/queries fields cleanup logic 2014-02-09 14:34:27 +02:00
Arik Fraimovich
f265d9174a Fix: POST api/queries fields cleanup logic 2014-02-09 14:34:27 +02:00
Arik Fraimovich
45ec489080 Fix: format_sql api call wasn't working. 2014-02-09 14:33:52 +02:00
Arik Fraimovich
970e0e2d04 Fix: format_sql api call wasn't working. 2014-02-09 14:33:52 +02:00
Arik Fraimovich
93fe613a9a Migratino to set Widget.type and Widget.query_id to nullables 2014-02-09 14:33:05 +02:00
Arik Fraimovich
9055865e1c Migratino to set Widget.type and Widget.query_id to nullables 2014-02-09 14:33:05 +02:00
Arik Fraimovich
704f2c176d Prefetching for widgets/visualizations/queries/query resutls when getting dashboard. 2014-02-08 21:16:36 +02:00
Arik Fraimovich
f9b6aca8e8 Prefetching for widgets/visualizations/queries/query resutls when getting dashboard. 2014-02-08 21:16:36 +02:00
Arik Fraimovich
d538134bb9 Bring back type to Widget definition. 2014-02-08 21:01:48 +02:00
Arik Fraimovich
d084b5a03c Bring back type to Widget definition. 2014-02-08 21:01:48 +02:00
Arik Fraimovich
6e38050ac4 Fix unicode representatino of Widget and Visualization models. 2014-02-08 21:01:21 +02:00
Arik Fraimovich
a6ab0ff2aa Fix unicode representatino of Widget and Visualization models. 2014-02-08 21:01:21 +02:00
Arik Fraimovich
f3c87ef313 Readme formatting 2014-02-06 21:27:43 +02:00
Arik Fraimovich
1bce924d83 Readme formatting 2014-02-06 21:27:43 +02:00
Arik Fraimovich
09a2136f02 Fix build status image link 2014-02-06 21:26:51 +02:00
Arik Fraimovich
f571e8ac6e Fix build status image link 2014-02-06 21:26:51 +02:00
Arik Fraimovich
5c7331d0a4 Coveralls badge 2014-02-06 21:25:18 +02:00
Arik Fraimovich
27bf2e642b Coveralls badge 2014-02-06 21:25:18 +02:00
Arik Fraimovich
187ea86c24 CirlceCI badge 2014-02-06 21:21:52 +02:00
Arik Fraimovich
d4ca903a07 CirlceCI badge 2014-02-06 21:21:52 +02:00
Arik Fraimovich
48639adc42 Create default visualization. 2014-02-06 21:12:02 +02:00
Arik Fraimovich
0f8bbdc9f2 Create default visualization. 2014-02-06 21:12:02 +02:00
Arik Fraimovich
509412dee6 Visualization API tests. 2014-02-06 21:02:14 +02:00
Arik Fraimovich
fb9f814b00 Visualization API tests. 2014-02-06 21:02:14 +02:00
Arik Fraimovich
44a95c4888 Use same database name in tests as CircleCI (until we add config for tests). 2014-02-06 21:02:13 +02:00
Arik Fraimovich
b4f88196dc Use same database name in tests as CircleCI (until we add config for tests). 2014-02-06 21:02:13 +02:00
Arik Fraimovich
0f3400a6b7 Update circle config to create settings.py file. 2014-02-06 21:02:13 +02:00
Arik Fraimovich
78e748548c Update circle config to create settings.py file. 2014-02-06 21:02:13 +02:00
Arik Fraimovich
a55bbc5e8c Tests for Query, Widget and Dashboard controllers. 2014-02-06 21:02:13 +02:00
Arik Fraimovich
199cddfbdb Tests for Query, Widget and Dashboard controllers. 2014-02-06 21:02:13 +02:00
Arik Fraimovich
8dad478a19 Factories for all models for tests. 2014-02-06 21:02:13 +02:00
Arik Fraimovich
c0ca602017 Factories for all models for tests. 2014-02-06 21:02:13 +02:00
Arik Fraimovich
31208c2af1 Update circle config to run python tests & cache packages. 2014-02-06 21:02:13 +02:00
Arik Fraimovich
3471b9853e Update circle config to run python tests & cache packages. 2014-02-06 21:02:13 +02:00
Arik Fraimovich
11f57b02e6 Migrations folder 2014-02-06 21:02:13 +02:00
Arik Fraimovich
6765d7d89f Migrations folder 2014-02-06 21:02:13 +02:00
Arik Fraimovich
86a99e2337 Fix: bring back support for MySQL 2014-02-06 21:02:13 +02:00
Arik Fraimovich
250aa17e63 Fix: bring back support for MySQL 2014-02-06 21:02:13 +02:00
Arik Fraimovich
3470d38d7c Visualization handlers. 2014-02-06 20:56:00 +02:00
Arik Fraimovich
2942d20ac3 Visualization handlers. 2014-02-06 20:56:00 +02:00
Arik Fraimovich
e6959e75f9 Add Visualization model. 2014-02-06 20:56:00 +02:00
Arik Fraimovich
d32799b2dc Add Visualization model. 2014-02-06 20:56:00 +02:00
Arik Fraimovich
1e4f70747b More tests (Dasboard API). 2014-02-06 20:56:00 +02:00
Arik Fraimovich
ff62fbbcf4 More tests (Dasboard API). 2014-02-06 20:56:00 +02:00
Arik Fraimovich
6ee3bc099d Fix: make sure all dashboard slug are unique 2014-02-06 20:56:00 +02:00
Arik Fraimovich
69ec362a8d Fix: make sure all dashboard slug are unique 2014-02-06 20:56:00 +02:00
Arik Fraimovich
13d44ee3e8 Create db task instead of SQL tables. 2014-02-06 20:56:00 +02:00
Arik Fraimovich
41d00543d0 Create db task instead of SQL tables. 2014-02-06 20:56:00 +02:00
Arik Fraimovich
fc9bffddbd Updated requirements.txt (flask-peewee). 2014-02-06 20:56:00 +02:00
Arik Fraimovich
f890e590e1 Updated requirements.txt (flask-peewee). 2014-02-06 20:56:00 +02:00
Arik Fraimovich
64d573e28e Add created_at to all models. (#10) 2014-02-06 20:56:00 +02:00
Arik Fraimovich
2aec982577 Add created_at to all models. (#10) 2014-02-06 20:56:00 +02:00
Arik Fraimovich
b2781a1ea6 Set needed fields as indexed. 2014-02-06 20:56:00 +02:00
Arik Fraimovich
b66d5daad0 Set needed fields as indexed. 2014-02-06 20:56:00 +02:00
Arik Fraimovich
04cdc75841 Remove MAX_CONNECTIONS from example settings 2014-02-06 20:56:00 +02:00
Arik Fraimovich
6ff07b99dc Remove MAX_CONNECTIONS from example settings 2014-02-06 20:56:00 +02:00
Arik Fraimovich
bb7bb40e76 Use peewee in data.Manager. 2014-02-06 20:56:00 +02:00
Arik Fraimovich
1586860e15 Use peewee in data.Manager. 2014-02-06 20:56:00 +02:00
Arik Fraimovich
a4055364e4 Remove Django from requirements.txt 2014-02-06 20:56:00 +02:00
Arik Fraimovich
99dac8f6fd Remove Django from requirements.txt 2014-02-06 20:56:00 +02:00
Arik Fraimovich
71da6e4528 Remove Django from config 2014-02-06 20:56:00 +02:00
Arik Fraimovich
5fb910b886 Remove Django from config 2014-02-06 20:56:00 +02:00
Arik Fraimovich
5c113284e2 Remove Django models 2014-02-06 20:56:00 +02:00
Arik Fraimovich
fb826ec838 Remove Django models 2014-02-06 20:56:00 +02:00
Arik Fraimovich
b2cb3bcf1d peewee based models 2014-02-06 20:55:14 +02:00
Arik Fraimovich
5198cc17d3 peewee based models 2014-02-06 20:55:14 +02:00
Arik Fraimovich
1821f90664 make test command 2014-02-06 20:55:14 +02:00
Arik Fraimovich
261ecfcb11 make test command 2014-02-06 20:55:14 +02:00
Arik Fraimovich
a66a8982ee Exclude settings.py from coverage report. 2014-02-06 20:55:14 +02:00
Arik Fraimovich
6582bce0d3 Exclude settings.py from coverage report. 2014-02-06 20:55:14 +02:00
Arik Fraimovich
0a83a1f168 Coverage & coveralls.io support 2014-02-06 20:55:14 +02:00
Arik Fraimovich
db91ca82c1 Coverage & coveralls.io support 2014-02-06 20:55:14 +02:00
Arik Fraimovich
e97d3172eb Initial version of tests. 2014-02-06 20:55:14 +02:00
Arik Fraimovich
cb7fbc16b0 Initial version of tests. 2014-02-06 20:55:14 +02:00
Arik Fraimovich
7c838bf54e Add .coverage file to gitignore 2014-02-06 20:55:14 +02:00
Arik Fraimovich
c6c639f16f Add .coverage file to gitignore 2014-02-06 20:55:14 +02:00
Arik Fraimovich
4a5c5143b3 Cleanup manage.py. 2014-02-06 20:55:14 +02:00
Arik Fraimovich
cb5968bc5f Cleanup manage.py. 2014-02-06 20:55:14 +02:00
Arik Fraimovich
c02afbb4f9 Remove commented out code. 2014-02-06 20:55:14 +02:00
Arik Fraimovich
693b25efc5 Remove commented out code. 2014-02-06 20:55:14 +02:00
Arik Fraimovich
b647bc9b41 This version of GoogleAuth has no force_auth_on_every_request option. 2014-02-06 20:55:14 +02:00
Arik Fraimovich
6eddaeda61 This version of GoogleAuth has no force_auth_on_every_request option. 2014-02-06 20:55:14 +02:00
Arik Fraimovich
c36b90db0f Remove debug print. 2014-02-06 20:55:13 +02:00
Arik Fraimovich
349bfa9139 Remove debug print. 2014-02-06 20:55:13 +02:00
Arik Fraimovich
ddf3959d4d Real HAMC authentication 2014-02-06 20:55:13 +02:00
Arik Fraimovich
b0f75678ee Real HAMC authentication 2014-02-06 20:55:13 +02:00
Arik Fraimovich
b5f88c199c Add API authentication support 2014-02-06 20:55:13 +02:00
Arik Fraimovich
0a0f7d7365 Add API authentication support 2014-02-06 20:55:13 +02:00
Arik Fraimovich
a0586457da Make manage.py executable. 2014-02-06 20:55:13 +02:00
Arik Fraimovich
6d1ff98bda Make manage.py executable. 2014-02-06 20:55:13 +02:00
Arik Fraimovich
288d1f7e5a Fix import issues (renamed api to controllers). 2014-02-06 20:55:13 +02:00
Arik Fraimovich
065324d256 Fix import issues (renamed api to controllers). 2014-02-06 20:55:13 +02:00
Arik Fraimovich
38c28bccdb Add server starting option to manage.py. 2014-02-06 20:55:13 +02:00
Arik Fraimovich
69f7c3417e Add server starting option to manage.py. 2014-02-06 20:55:13 +02:00
Arik Fraimovich
e8b0178ae4 Update requirements.txt. 2014-02-06 20:55:13 +02:00
Arik Fraimovich
806f57c627 Update requirements.txt. 2014-02-06 20:55:13 +02:00
Arik Fraimovich
9eeebf93fa Remove code duplications 2014-02-06 20:55:13 +02:00
Arik Fraimovich
e4c7844cae Remove code duplications 2014-02-06 20:55:13 +02:00
Arik Fraimovich
c1ccf02ff9 Move manage cli into top level 2014-02-06 20:55:13 +02:00
Arik Fraimovich
6ebfa16740 Move manage cli into top level 2014-02-06 20:55:13 +02:00
Arik Fraimovich
6533aa2826 Cleanup the api module 2014-02-06 20:55:13 +02:00
Arik Fraimovich
43cfdb8727 Cleanup the api module 2014-02-06 20:55:13 +02:00
Arik Fraimovich
ece1a51530 Replace Tornado with Flask 2014-02-06 20:55:13 +02:00
Arik Fraimovich
b31c5be70e Replace Tornado with Flask 2014-02-06 20:55:13 +02:00
Arik Fraimovich
1d4a407161 Rename rd_service to redash. 2014-02-06 20:51:51 +02:00
Arik Fraimovich
d84d047470 Rename rd_service to redash. 2014-02-06 20:51:51 +02:00
Arik Fraimovich
9f5678c711 Merge pull request #83 from EverythingMe/viz
Visualization Followups + workers bugfix
2014-02-06 19:46:28 +02:00
Arik Fraimovich
42a0659012 Merge pull request #83 from EverythingMe/viz
Visualization Followups + workers bugfix
2014-02-06 19:46:28 +02:00
Amir Nissim
819ac84c2a fix issue where start_workers failed when settings.CONNECTION_ADAPTER does not exist 2014-02-06 16:40:58 +02:00
Amir Nissim
6386f0f9aa fix issue where start_workers failed when settings.CONNECTION_ADAPTER does not exist 2014-02-06 16:40:58 +02:00
Amir Nissim
fe90f3703e Fixes #80:
* Create default 'Table' visualization for all queries
 * remove 'Table' type when creating new visualization
 * Set type as the default visualization name (instead of the query name)
 * Remove description field and advanced mode
 * Remove section for adding new visualization in new widget dialog
2014-02-06 16:35:29 +02:00
Amir Nissim
9aaf17d478 Fixes #80:
* Create default 'Table' visualization for all queries
 * remove 'Table' type when creating new visualization
 * Set type as the default visualization name (instead of the query name)
 * Remove description field and advanced mode
 * Remove section for adding new visualization in new widget dialog
2014-02-06 16:35:29 +02:00
Arik Fraimovich
0e956a605f Merge pull request #79 from EverythingMe/viz
fix migration to set 'bars' as default
2014-02-05 17:55:23 +02:00
Arik Fraimovich
1f908f9040 Merge pull request #79 from EverythingMe/viz
fix migration to set 'bars' as default
2014-02-05 17:55:23 +02:00
Amir Nissim
32210d89f8 fix migration to set 'bars' as default 2014-02-05 17:54:12 +02:00
Amir Nissim
b51ef059f5 fix migration to set 'bars' as default 2014-02-05 17:54:12 +02:00
Arik Fraimovich
18a77c995f Exclude venv dir from the release package 2014-02-05 09:48:11 +02:00
Arik Fraimovich
a9e135c94f Exclude venv dir from the release package 2014-02-05 09:48:11 +02:00
Arik Fraimovich
9f36234c52 Add query_id back to widgets in tables.sql until we remove it from Model 2014-02-05 09:47:51 +02:00
Arik Fraimovich
212ade2da7 Add query_id back to widgets in tables.sql until we remove it from Model 2014-02-05 09:47:51 +02:00
Arik Fraimovich
0b74d9e998 Merge pull request #76 from EverythingMe/fix_75
Fix #75: Large numbers shown as NaN/NaN/NaN NaN:NaN
2014-02-04 23:45:01 -08:00
Arik Fraimovich
54d545094f Remove backward compatability workaround (fixes #75) 2014-02-05 09:41:03 +02:00
Arik Fraimovich
c239c476af Merge pull request #74 from EverythingMe/update_readme
Readme update (added reference to mailing list & IRC channel)
2014-02-04 07:37:04 -08:00
Arik Fraimovich
a382a0cd44 Small fix to README. 2014-02-04 17:02:24 +02:00
Arik Fraimovich
0fee59a6ed Merge pull request #73 from EverythingMe/viz
Visualization Support
2014-02-04 07:01:40 -08:00
Arik Fraimovich
e18226d108 Readme update (added reference to mailing list & IRC channel) 2014-02-04 16:58:36 +02:00
Amir Nissim
b079952491 version bump 0.2 2014-02-04 16:56:35 +02:00
Amir Nissim
d2da71c22a migrating Widgets to Visualizations 2014-02-04 16:11:48 +02:00
Amir Nissim
9eb2a6a535 Visualization.name length to 255 (should match Query.name length) 2014-02-04 15:16:07 +02:00
Amir Nissim
dd5ef7ec72 add Visualization and SERIES types 2014-02-03 16:35:16 +02:00
Amir Nissim
c2cbcd3727 Dashboard visualizations 2014-02-03 16:12:29 +02:00
Amir Nissim
5c7baf9e05 QueryFiddle: Live chart type editing 2014-02-03 15:01:41 +02:00
Amir Nissim
e5f5e18ecc Live visualization config POC (title only) 2014-02-02 18:20:18 +02:00
Amir Nissim
dae30037b6 delete visualizations 2014-02-02 13:23:01 +02:00
Amir Nissim
30eba3bfae edit and create visualizations 2014-02-02 13:23:01 +02:00
Amir Nissim
77c0486f8c create Visualization cont. 2014-02-02 13:23:01 +02:00
Amir Nissim
e00475520a create Visualization [WIP] 2014-02-02 13:23:01 +02:00
Amir Nissim
bf90a6247e Visualization models 2014-02-02 13:23:01 +02:00
Amir Nissim
3185cc041a Visualization UI:
* queryfiddle page
 * new widget form
2014-02-02 13:23:01 +02:00
Arik Fraimovich
f64b9084f5 Merge pull request #69 from ekampf/feature/mysql
MySQL Support
2014-01-30 06:20:58 -08:00
Eran Kampf
dc09561f30 Fixed MySQL Runner 2014-01-30 16:15:03 +02:00
Eran Kampf
e154cbe1ba Redshift shouldn't be here 2014-01-30 16:03:58 +02:00
Eran Kampf
1f9ac49e27 Removed unnecessary logging 2014-01-30 11:28:49 +02:00
Eran Kampf
a7de923cea Returned redshift code 2014-01-30 11:28:11 +02:00
Eran Kampf
a75430106e Merge branch 'refs/heads/master' into feature/mysql 2014-01-30 11:21:33 +02:00
Eran Kampf
bc816100a0 Removed unecessary logging 2014-01-29 21:02:12 +02:00
Eran Kampf
33de209497 Separated query runners to diff files 2014-01-29 20:57:09 +02:00
Eran Kampf
8401e25504 Include MySQL example 2014-01-29 19:30:59 +02:00
Eran Kampf
db14c695e6 MySQL query runner 2014-01-29 19:02:21 +02:00
Arik Fraimovich
7a61b2ec80 Merge pull request #66 from EverythingMe/bug-9
Dashboard: update layout editor when adding/removing widgets. fixes #9
2014-01-26 07:28:54 -08:00
Arik Fraimovich
1e16e58f37 Fix to upload script 2014-01-26 17:04:12 +02:00
Arik Fraimovich
e84ca44178 Use only filename; without path 2014-01-26 16:59:59 +02:00
Arik Fraimovich
644c03503b More explicit python version 2014-01-26 16:54:32 +02:00
Arik Fraimovich
d88288302a Set Python version 2014-01-26 16:51:07 +02:00
Arik Fraimovich
42e0797b5b Install requests in CircleCI 2014-01-26 16:34:31 +02:00
Arik Fraimovich
8826d41922 Cirlce: upload file to GitHub when done. 2014-01-26 16:30:16 +02:00
Arik Fraimovich
26d2d6f403 Add version to gzip file 2014-01-26 16:26:46 +02:00
Arik Fraimovich
438386de5d Upload version to github script 2014-01-26 16:26:32 +02:00
Arik Fraimovich
99197396f1 Merge pull request #67 from EverythingMe/circleci
CircleCI configuration & makefile.
2014-01-25 23:28:15 -08:00
Arik Fraimovich
3770463499 CircleCI configuration & makefile. 2014-01-26 09:23:39 +02:00
Amir Nissim
d3979a5a5a Dashboard: update layout editor when adding/removing widgets. fixes #9 2014-01-23 18:12:44 +02:00
Arik Fraimovich
e5bba73ea8 Merge pull request #65 from EverythingMe/bug-33
QueryFiddle: reset sorting when executing query. fixes #33
2014-01-23 05:35:34 -08:00
Arik Fraimovich
cd925d1896 Merge pull request #56 from EverythingMe/feature-queryform
queryFiddle: reset form state if changes have been reverted to original values (no ngForm)
2014-01-23 05:35:11 -08:00
Amir Nissim
82fe6f6fa7 QueryFiddle: reset sorting when executing query. fixes #33 2014-01-23 14:57:38 +02:00
Arik Fraimovich
c05cf29a37 Pass the widget options object as is. 2014-01-22 09:27:01 +02:00
Arik Fraimovich
160f491cc5 Option to control chart type 2014-01-22 08:56:54 +02:00
Amir Nissim
d652013572 queryFiddle: reset form state if changes have been reverted to original values (no ngForm) 2014-01-16 16:10:12 +02:00
Arik Fraimovich
c970503f61 Merge pull request #55 from EverythingMe/feature-queryform
use ngForm in queryFiddle page for detecting pristine and dirty states
2014-01-16 04:07:12 -08:00
Amir Nissim
5218f4f182 use ngForm in queryFiddle page for detecting pristine and dirty states 2014-01-16 13:44:13 +02:00
Amir Nissim
9230a77f96 edit-in-place: set ng-class inside directive 2014-01-15 17:02:23 +02:00
Arik Fraimovich
f8cc78eca5 Show link button on dashboard 2014-01-15 15:05:19 +02:00
Arik Fraimovich
a9f9af3cb8 Merge pull request #53 from EverythingMe/feature_description
Updates to edit-in-place directive & showing description in dashboard
2014-01-15 04:42:36 -08:00
Arik Fraimovich
ec71621d93 Make margin margin after description smaller 2014-01-15 14:35:56 +02:00
Amir Nissim
52376993df edit-in-place: editable attr 2014-01-15 13:59:11 +02:00
Arik Fraimovich
74a5253c69 Show description in dashboard. 2014-01-15 11:38:38 +02:00
Arik Fraimovich
2aebc023d1 Show edit-in-place only if the user can edit. 2014-01-15 11:38:30 +02:00
Arik Fraimovich
8dfd453381 Change look of edit in place text area. 2014-01-15 11:15:35 +02:00
Arik Fraimovich
899cb9d4cf Merge pull request #52 from EverythingMe/dev
Upgrade angular + editing query description
2014-01-14 11:33:38 -08:00
Amir Nissim
e34021c0be Add ability to edit query description (FED #22) 2014-01-14 17:10:55 +02:00
Arik Fraimovich
041d5da13b Fix: default predicate should be undefined and not empty string 2014-01-14 17:10:55 +02:00
Amir Nissim
d421848795 upgrade: angular v1.2.7 2014-01-14 17:10:55 +02:00
Arik Fraimovich
96185e9c60 Add semicolon 2014-01-14 09:32:23 +02:00
Arik Fraimovich
5bd8ef2e5d Fix: homepage was skipping dashboards in other group 2014-01-14 09:24:17 +02:00
Arik Fraimovich
3dae7e9523 use currentUser.canEdit in more places 2014-01-14 09:11:19 +02:00
Arik Fraimovich
7d4660173e Show warning of leaving the page only if the user can edit the query. 2014-01-14 09:03:04 +02:00
Arik Fraimovich
612c6a331b Only unbind save shortcut if we're actually leaving the page. 2014-01-14 09:02:10 +02:00
Arik Fraimovich
0c852a145e Only save query if the user can edit it. 2014-01-14 09:01:42 +02:00
Arik Fraimovich
ed2d3a27e7 Add canEdit function to user object.
Checks if currentUser included in the user string the provided object. The reason I check inclusion and not equality, is to support scenario of multiple users.
2014-01-14 09:01:20 +02:00
Arik Fraimovich
de162817af Add the option to specify analytics code to inject into the template. 2014-01-13 16:52:35 +02:00
Arik Fraimovich
fd1acd6533 Save query when pressing Cmd+S. 2014-01-11 20:04:27 +02:00
Arik Fraimovich
7282f61133 Fix: don't show save warning, when switching tabs. 2014-01-11 20:04:12 +02:00
Arik Fraimovich
0687d9ed98 Merge pull request #48 from EverythingMe/feature_query_api_key
Feature: allow downloading CSV of a query by using an API key
2014-01-04 02:00:15 -08:00
Arik Fraimovich
e45a3ebdb4 Allow downloading CSV for unauthenticated users with api_key. 2014-01-04 11:53:45 +02:00
Arik Fraimovich
b72f9f054d Allow downloading CSV by query id and not just query_result id. 2014-01-04 11:21:52 +02:00
Arik Fraimovich
92b9fb60e9 Add api_key field to queries. 2014-01-04 11:09:13 +02:00
Arik Fraimovich
08951ab515 Add BaseAuthenticatedHandler and move authentication logic there. 2014-01-04 10:55:40 +02:00
Arik Fraimovich
c2d2bd0ea1 Fix cookbook path 2014-01-04 10:52:21 +02:00
Arik Fraimovich
ff6204c98e Move vagrant related files to top level 2014-01-04 10:51:05 +02:00
Timor Raiman
c08831ca13 In Vagrantfile, support skiping tarball download, and graft the current development tree in stead 2014-01-04 10:51:04 +02:00
Timor Raiman
c8ef72e4d2 Correct postgresql encrypted vs cleartext passwords 2014-01-04 10:51:04 +02:00
Timor Raiman
b1bd52423a Use md5 digest of readable pg password postgres user 2014-01-04 10:51:04 +02:00
Timor Raiman
4b980b8076 Force postgresql::server in Vagrant 2014-01-04 10:51:04 +02:00
Timor Raiman
63baa20403 Improve Vagrantfile 2014-01-04 10:51:04 +02:00
Timor Raiman
612aca217c Code review by Yoni 2014-01-04 10:51:04 +02:00
Timor Raiman
92b56c99a3 Remove chef cookbooks (migrated to separate repo) 2014-01-04 10:51:04 +02:00
Timor Raiman
349b18d63a Cleanup 2014-01-04 10:51:04 +02:00
Timor Raiman
11d331c051 Still hitting the encoding conversion error 2014-01-04 10:51:04 +02:00
Timor Raiman
63851b16af Add .DS_Store to .gitignore 2014-01-04 10:51:04 +02:00
Timor
4384eed09f add vanila vagrant configuration 2014-01-04 10:51:04 +02:00
Arik Fraimovich
e746805eaa Merge pull request #47 from EverythingMe/fed-issue1
FED issue #1 [r=arikfr]
2013-12-31 01:05:04 -08:00
Amir Nissim
6c480178fe Show confirm box, when trying to leave the page before saving the query (FED #1) 2013-12-30 16:34:48 +02:00
Amir Nissim
7e94cc7ff8 Show indication when query has unsaved changes (FED #1) 2013-12-30 15:11:49 +02:00
Amir Nissim
db20eeb555 Show error notification when saving a query fails (FED #1) 2013-12-30 14:32:07 +02:00
Amir Nissim
9794f12a9b harcode dependencies 2013-12-29 18:25:50 +02:00
Arik Fraimovich
9af88076e6 Limit page title size 2013-12-26 14:09:59 +02:00
Arik Fraimovich
290ae85128 You can't have dashes in object properties. Fixes #42 2013-12-23 21:59:01 +02:00
Arik Fraimovich
5c78760649 Prevent from setting a blank query title. Fixes #34 2013-12-23 21:56:01 +02:00
Arik Fraimovich
3cb8365ef3 Escape % in column names. Fixes #42 2013-12-23 21:33:11 +02:00
Arik Fraimovich
38e95a7f07 Dashboard menu has sub-menus now 2013-12-23 21:23:52 +02:00
Arik Fraimovich
6d392b1c91 having values ordered by Y, messes up the data so removing this 2013-12-22 17:23:39 +02:00
Arik Fraimovich
a8f7028c22 Merge branch 'bug_workers_dying' 2013-12-22 17:05:44 +02:00
Arik Fraimovich
35c7366b96 Fix: need to make sure that each category has a value 2013-12-22 17:05:40 +02:00
Arik Fraimovich
137bd43821 Add afork to mitigate issues of mixing threading and forking 2013-12-18 09:40:48 +02:00
Arik Fraimovich
08c9a0630d Take only needed vals for connection params 2013-12-17 12:46:06 +02:00
Arik Fraimovich
abdc9f75cc More places where I need to use redis_connection 2013-12-17 12:19:50 +02:00
Arik Fraimovich
ecaae1b934 Run annotated queries. 2013-12-17 12:03:10 +02:00
Arik Fraimovich
fc06f8c88e Change Job to use redis connection instead of data manager & use own redis connection in forked process. 2013-12-17 12:02:58 +02:00
Arik Fraimovich
0fc62f07cc Set proctitle for worker 2013-12-16 18:52:30 +02:00
Arik Fraimovich
4afb12669a Add support for refreshing a query once a week 2013-12-13 18:46:14 +02:00
Arik Fraimovich
030864b72b Humanize query runtime 2013-12-08 15:44:46 +02:00
Arik Fraimovich
0bf6e39c66 Improvements to queries page: (#6)
1. Search (client side).
2. Stats about queries.
3. Pagination.
2013-12-08 15:26:35 +02:00
Arik Fraimovich
0d6613b998 Return query stats (runtime and such) in API. 2013-12-08 15:25:44 +02:00
Arik Fraimovich
99875ff746 Bring back browser notifications (#1) 2013-12-07 14:18:39 +02:00
Arik Fraimovich
05bb0fcf43 Support for non date time x values. 2013-12-07 13:14:02 +02:00
Arik Fraimovich
bce60758e9 No more using connection pool in DataManager, as it used accross processes 2013-12-06 20:56:08 +02:00
Arik Fraimovich
7b85e78636 Add log_level to settings_example 2013-12-06 17:52:13 +02:00
Arik Fraimovich
4fa6ef828c More readable status 2013-12-06 15:50:02 +02:00
Arik Fraimovich
08ca3431ac Jobs done counter was updated in the wrong place 2013-12-06 15:32:36 +02:00
Arik Fraimovich
cfcc21b1cb Improved system status. 2013-12-06 15:14:39 +02:00
Arik Fraimovich
4ea54ef5ce Instead of killing the process, send SIGINT and cancel query on interupt. 2013-12-01 11:52:14 +02:00
Arik Fraimovich
fc65920462 Show cancel button in UI. 2013-11-29 21:30:03 +02:00
Arik Fraimovich
88a7ff62af Job: when cancelling send SIGKILL instead of SIGINT. 2013-11-29 21:30:03 +02:00
Arik Fraimovich
1c75ae08bc Job: support for old job that had no process id. 2013-11-29 21:30:03 +02:00
Arik Fraimovich
5ea63534f7 Naive implementation of job cancel 2013-11-29 21:30:03 +02:00
Arik Fraimovich
95805169dc Store job process id 2013-11-29 21:30:03 +02:00
Arik Fraimovich
bcd018d8de If worker forked process didn't exit cleanly, update job 2013-11-29 21:30:02 +02:00
Arik Fraimovich
34627f5e60 Don't use connection pool in query runner 2013-11-29 21:30:02 +02:00
Arik Fraimovich
0ae1692f99 Fix: child process wasn't exiting 2013-11-29 21:30:02 +02:00
Arik Fraimovich
6becbee27a Run query in forked process 2013-11-29 21:30:02 +02:00
Arik Fraimovich
78633b06de Log exception when refresh queries fails 2013-11-29 21:30:02 +02:00
Arik Fraimovich
78bf265d7a Fix: use String ctor function instead of toString to handle nulls 2013-11-29 21:30:02 +02:00
Arik Fraimovich
1690a25262 Fix: use String ctor function instead of toString to handle nulls 2013-11-24 16:06:01 +02:00
Arik Fraimovich
f76f284ce2 Add support for displaying cohorts in dashbaords 2013-11-17 13:41:54 +02:00
Arik Fraimovich
5080b754d4 Merge pull request #36 from shayel/master
Fixed installation dependencies, instructions
2013-11-07 10:09:23 -08:00
Arik Fraimovich
bdb97182e4 Fixes #38: getChartData returns sorted data by x 2013-11-06 15:24:41 +02:00
Arik Fraimovich
c668ed8a2b Show shared tooltip & percentage 2013-11-06 15:03:08 +02:00
Arik Fraimovich
10a1350bb3 Support multiple series on one row 2013-11-05 13:50:27 +02:00
Arik Fraimovich
c10fb2916f Fix: when series name was 0 (the number) it would use the y name instead 2013-11-05 13:27:28 +02:00
Arik Fraimovich
91185abb4c Update title to use | instead of : 2013-11-04 17:11:45 +02:00
Arik Fraimovich
e402b06c6c Make qr take redis connection params from the general redis connection 2013-11-04 15:52:48 +02:00
Shay Elkin
6a09adf11c Take specific grunt commit (that fixes gh-886) 2013-11-02 15:15:31 +02:00
Shay Elkin
ba7ba751fd Fixed installation dependencies, instructions
* rd_ui/package.json:
  * Add missing bower, grunt-cli dependencies
  * Take grunt from HEAD, as 0.4.1 has broken dependency: 08a3af53ff

* README.md:
  * Update instructions
2013-10-31 17:19:13 +02:00
Arik Fraimovich
ba3c02c912 Update README.md 2013-10-31 12:22:45 +02:00
Arik Fraimovich
6f6bd256b5 Update README.md 2013-10-30 18:25:17 +02:00
Arik Fraimovich
c8d1780ee8 Fix: columns with multiple spaces were not showing correctly 2013-10-30 17:45:26 +02:00
Arik Fraimovich
31e904c21a Fix: move utf deocding to proper place 2013-10-30 17:38:05 +02:00
Arik Fraimovich
6773488644 Fix: when query had unicode characters it was failling to execute 2013-10-30 17:11:00 +02:00
Arik Fraimovich
84b0d52510 Don't send query result when saving a query 2013-10-30 16:51:08 +02:00
Arik Fraimovich
db9aa4bc38 Fix #18: don't retrieve query results when already have them & ttl = -1. 2013-10-30 16:43:24 +02:00
Arik Fraimovich
04e1534001 Fix: stop sending latest_query_data back to the server. Fixes #31 2013-10-30 16:43:24 +02:00
Arik Fraimovich
74d4928fb0 Add getting started instructions. 2013-10-30 13:10:16 +02:00
Arik Fraimovich
d31d422eb0 Add screenshots to README. 2013-10-30 12:37:33 +02:00
Arik Fraimovich
eb5b62b670 Add dist folder to gitignore 2013-10-30 12:33:36 +02:00
Arik Fraimovich
53ef4fee1e Update README.md 2013-10-30 12:23:49 +02:00
Arik Fraimovich
b3cdc4f5fc Add link to demo instance. 2013-10-30 12:23:16 +02:00
Arik Fraimovich
63abb61248 Merge pull request #35 from amirnissim/patch-1
Update README.md
2013-10-30 01:13:07 -07:00
Amir Nissim
59e16866fb Update README.md
fixed broken Tornado link
2013-10-30 10:08:22 +02:00
Arik Fraimovich
9fc36bd6fa Group dashboards by category. 2013-10-29 19:08:11 +02:00
Arik Fraimovich
4051fae33b Enable cohorts to all users 2013-10-29 09:13:07 +02:00
Arik Fraimovich
b014dadfe3 Fix numbers format in grid. 2013-10-28 22:01:18 +02:00
Arik Fraimovich
900b084156 Fix: sorting on number/dates columns was wrong 2013-10-28 21:52:21 +02:00
Arik Fraimovich
fa96c94085 Move cookie secret to settings. #7 2013-10-28 19:48:20 +02:00
Arik Fraimovich
bd1d287c87 Fix use min configuration not to depend on the bower_components directory in dist. 2013-10-28 19:34:51 +02:00
Arik Fraimovich
b74f7e4eac Add resolutions to bower.json. 2013-10-28 18:24:41 +02:00
Arik Fraimovich
7a57132c1c updated roadmap 2013-10-28 16:51:49 +02:00
Arik Fraimovich
46c2367e50 README: fix link to new issue. 2013-10-28 15:23:10 +02:00
Arik Fraimovich
7378f85297 Link to the license file in README. 2013-10-28 15:21:03 +02:00
137 changed files with 8237 additions and 3719 deletions

5
.coveragerc Normal file
View File

@@ -0,0 +1,5 @@
[report]
omit =
*/settings.py
*/python?.?/*
*/site-packages/nose/*

9
.env.example Normal file
View File

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

14
.gitignore vendored
View File

@@ -1,10 +1,20 @@
.coveralls.yml
.idea
*.pyc
rd_service/settings.py
.coverage
rd_ui/dist
.DS_Store
celerybeat-schedule*
.#*
\#*#
*~
# Vagrant related
.vagrant
Berksfile.lock
rd_service/dump.rdb
redash/dump.rdb
.env
.ruby-version
venv
dump.rdb

View File

@@ -1,3 +0,0 @@
cookbook 'apt'
cookbook 'postgresql'
cookbook 'redash', git: 'git@github.com:EverythingMe/chef-redash.git'

View File

@@ -1,16 +1,22 @@
NAME=redash
VERSION=0.2
FULL_VERSION=$(VERSION).$(CIRCLE_BUILD_NUM)
FILENAME=$(CIRCLE_ARTIFACTS)/$(NAME).$(FULL_VERSION).tar.gz
VERSION=`python ./manage.py version`
FULL_VERSION=$(VERSION)+b$(CIRCLE_BUILD_NUM)
# VERSION gets evaluated every time it's referenced, therefore we need to use VERSION here instead of FULL_VERSION.
FILENAME=$(CIRCLE_ARTIFACTS)/$(NAME).$(VERSION).tar.gz
deps:
cd rd_ui && npm install
cd rd_ui && npm install grunt-cli bower
cd rd_ui && npm install -g bower grunt-cli
cd rd_ui && bower install
cd rd_ui && grunt build
pack:
tar -zcv -f $(FILENAME) --exclude=".git*" --exclude="*.pyc" --exclude="*.pyo" --exclude="rd_ui/node_modules" --exclude="rd_ui/dist/bower_components" --exclude="rd_ui/app" *
sed -ri "s/^__version__ = '([0-9.]*)'/__version__ = '$(FULL_VERSION)'/" redash/__init__.py
tar -zcv -f $(FILENAME) --exclude=".git*" --exclude="*.pyc" --exclude="*.pyo" --exclude="venv" --exclude="rd_ui/node_modules" --exclude="rd_ui/dist/bower_components" --exclude="rd_ui/app" *
upload:
python bin/upload_version.py $(FULL_VERSION) $(FILENAME)
python bin/upload_version.py $(VERSION) $(FILENAME)
test:
nosetests --with-coverage --cover-package=redash tests/*.py
cd rd_ui && grunt test

1
Procfile Normal file
View File

@@ -0,0 +1 @@
web: honcho start -f Procfile.heroku -p $PORT

2
Procfile.dev Normal file
View File

@@ -0,0 +1,2 @@
web: ./manage.py runserver -p $PORT
worker: ./bin/run celery worker --app=redash.worker --beat -Qqueries,celery,scheduled_queries

2
Procfile.heroku Normal file
View File

@@ -0,0 +1,2 @@
web: ./manage.py runserver -p $PORT --host 0.0.0.0 -d -r
worker: ./bin/run celery worker --app=redash.worker --beat -Qqueries,celery,scheduled_queries

View File

@@ -1,4 +1,5 @@
# [_re:dash_](https://github.com/everythingme/redash)
![Build Status](https://circleci.com/gh/EverythingMe/redash.png?circle-token=8a695aa5ec2cbfa89b48c275aea298318016f040 "Build Status")
**_re:dash_** is our take on freeing the data within our company in a way that will better fit our culture and usage patterns.
@@ -28,8 +29,8 @@ Due to Heroku dev plan limits, it has a small database of flights (see schema [h
## Technology
* Python
* [AngularJS](http://angularjs.org/)
* [Tornado](http://tornadoweb.org)
* [PostgreSQL](http://www.postgresql.org/) / [AWS Redshift](http://aws.amazon.com/redshift/)
* [Redis](http://redis.io)
@@ -45,62 +46,21 @@ It's very likely that in the future we will switch to [D3.js](http://d3js.org/)
## Getting Started
1. Clone the repo:
```bash
git clone git@github.com:EverythingMe/redash.git
```
2. Create settings file from the example one (& update relevant settings):
```bash
cp rd_service/settings_example.py rd_service/settings.py
```
It's highly recommended that the user you use to connect to the data database (the one you query) is read-only.
3. Create the operational databases from rd_service/data/tables.sql
3. Install `npm` packages (mainly: Bower & Grunt):
```bash
cd rd_ui
npm install
```
4. Install `bower` packages:
```bash
bower install
```
5. Build the UI:
```bash
grunt build
```
6. Install PIP packages:
```bash
pip install -r ../rd_service/requirements.txt
```
6. Start the API server:
```bash
cd ../rd_service
python server.py
```
7. Start the workers:
```bash
python cli.py worker
```
8. Open `http://localhost:8888/` and query away.
* [Setting up re:dash on Heroku in 5 minutes](https://github.com/EverythingMe/redash/wiki/Setting-up-re:dash-on-Heroku-in-5-minutes)
* [Setting re:dash on your own server (Ubuntu)](https://github.com/EverythingMe/redash/wiki/Setting-re:dash-on-your-own-server-(Ubuntu))
**Need help setting re:dash or one of the dependencies up?** Ping @arikfr on the IRC #redash channel or send a message to the [mailing list](https://groups.google.com/forum/#!forum/redash-users), and he will gladly help.
## Roadmap
We plan to release new minor version every 2-3 weeks. Of course, if we get additional help from contributors it will help speed things up.
Below you can see the "big" features of the next 3 releases (for full list, click on the link):
### [v0.2](https://github.com/EverythingMe/redash/issues?milestone=1&state=open)
- Ability to generate multiple visualizations for a single query (dataset) in a more flexible way than today. Also easier extensbility points to add additional visualizations.
- Dashboard filters: ability to filter/slice the data you see in a single dashboard using filters (date or selectors).
- UI Improvements (better notifications & flows, improved queries page)
- Comments on queries.
### [v0.3](https://github.com/EverythingMe/redash/issues?milestone=2&state=open)
- Support for API access using API keys, instead of Google Login.
- Dashboard filters: ability to filter/slice the data you see in a single dashboard using filters (date or selectors).
- Multiple databases support (including other database type than PostgreSQL).
- Scheduled reports by email.
- Comments on queries.
### [v0.4](https://github.com/EverythingMe/redash/issues?milestone=3&state=open)

60
Vagrantfile vendored
View File

@@ -1,60 +0,0 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
VAGRANTFILE_API_VERSION = '2'
POSTGRES_PASSWORD = 'securepass'
# Currently, chef postgress cookbook works with cleartext paswords,
# unless the password begins with 'md5'
# See https://github.com/hw-cookbooks/postgresql/issues/95
require "digest/md5"
postgres_password_md5 = 'md5'+Digest::MD5.hexdigest(POSTGRES_PASSWORD+'postgres')
# After starting the vagrant machine, the application is accessible via the URL
# http://localhost:9999
HOST_PORT_TO_FORWARD_TO_REDASH = 9999
# Deploy direcly the code in parent dir; Don't download a release tarball
live_testing_deployment = true
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.box = 'ubuntu-precise-cloudimg-amd64'
config.vm.box_url = 'http://cloud-images.ubuntu.com/vagrant/precise/current/precise-server-cloudimg-amd64-vagrant-disk1.box'
if config.respond_to? :cache
config.cache.auto_detect = true
end
config.berkshelf.enabled = true
config.omnibus.chef_version = :latest
config.vm.network 'forwarded_port', guest: 8888, host: HOST_PORT_TO_FORWARD_TO_REDASH
if live_testing_deployment
config.vm.synced_folder "..", "/opt/redash"
end
config.vm.provision :chef_solo do |chef|
# run apt-get update before anything else (specifically postgresql)..
chef.add_recipe 'apt'
chef.add_recipe 'redash::redis_for_redash'
chef.add_recipe 'postgresql::client'
chef.add_recipe 'postgresql::server'
chef.add_recipe 'redash::redash_pg_schema'
chef.add_recipe 'redash::redash'
# chef.log_level = :debug
chef.json = {
'apt' => { 'compiletime' => true },
'postgresql' => { 'password' => {'postgres' => postgres_password_md5 } },
'redash' => { 'db' => {'host' => 'localhost',
'user' => 'postgres',
'password' => POSTGRES_PASSWORD },
'allow' => {'google_app_domain' => 'gmail.com',
'admins' => ['joe@egmail.com','jack@gmail.com']},
'install_tarball' => !live_testing_deployment,
'user' => live_testing_deployment ? 'vagrant' : 'redash'}
}
end
end

30
bin/latest_release.py Executable file
View File

@@ -0,0 +1,30 @@
#!/usr/bin/env python
import sys
import requests
if __name__ == '__main__':
response = requests.get('https://api.github.com/repos/EverythingMe/redash/releases')
if response.status_code != 200:
exit("Failed getting releases (status code: %s)." % response.status_code)
sorted_releases = sorted(response.json(), key=lambda release: release['id'], reverse=True)
latest_release = sorted_releases[0]
asset_url = latest_release['assets'][0]['url']
filename = latest_release['assets'][0]['name']
wget_command = 'wget --header="Accept: application/octet-stream" %s -O %s' % (asset_url, filename)
if '--url-only' in sys.argv:
print asset_url
elif '--wget' in sys.argv:
print wget_command
else:
print "Latest release: %s" % latest_release['tag_name']
print latest_release['body']
print "\nTarball URL: %s" % asset_url
print 'wget: %s' % (wget_command)

10
bin/run Executable file
View File

@@ -0,0 +1,10 @@
#!/usr/bin/env bash
# Ideally I would use stdin with source, but in older bash versions this
# wasn't supported properly.
TEMP_ENV_FILE=`mktemp /tmp/redash_env.XXXXXX`
sed 's/^REDASH/export REDASH/' .env > $TEMP_ENV_FILE
source $TEMP_ENV_FILE
rm $TEMP_ENV_FILE
exec "$@"

View File

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

View File

@@ -3,29 +3,44 @@ import os
import sys
import json
import requests
import subprocess
def capture_output(command):
proc = subprocess.Popen(command, stdout=subprocess.PIPE)
return proc.stdout.read()
if __name__ == '__main__':
version = sys.argv[1]
filepath = sys.argv[2]
filename = filepath.split('/')[-1]
github_token = os.environ['GITHUB_TOKEN']
auth = (github_token, 'x-oauth-basic')
commit_sha = os.environ['CIRCLE_SHA1']
version = sys.argv[1]
filepath = sys.argv[2]
filename = filepath.split('/')[-1]
github_token = os.environ['GITHUB_TOKEN']
auth = (github_token, 'x-oauth-basic')
commit_sha = os.environ['CIRCLE_SHA1']
params = json.dumps({
'tag_name': 'v{0}'.format(version),
'name': 're:dash v{0}'.format(version),
'target_commitish': commit_sha
})
commit_body = capture_output(["git", "log", "--format=%b", "-n", "1", commit_sha])
file_md5_checksum = capture_output(["md5sum", filepath]).split()[0]
file_sha256_checksum = capture_output(["sha256sum", filepath]).split()[0]
version_body = "%s\n\nMD5: %s\nSHA256: %s" % (commit_body, file_md5_checksum, file_sha256_checksum)
response = requests.post('https://api.github.com/repos/everythingme/redash/releases',
data=params,
auth=auth)
params = json.dumps({
'tag_name': 'v{0}'.format(version),
'name': 're:dash v{0}'.format(version),
'body': version_body,
'target_commitish': commit_sha,
'prerelease': True
})
upload_url = response.json()['upload_url']
upload_url = upload_url.replace('{?name}', '')
response = requests.post('https://api.github.com/repos/everythingme/redash/releases',
data=params,
auth=auth)
with open(filepath) as file_content:
headers = {'Content-Type': 'application/gzip'}
response = requests.post(upload_url, file_content, params={'name': filename}, auth=auth, headers=headers, verify=False)
upload_url = response.json()['upload_url']
upload_url = upload_url.replace('{?name}', '')
with open(filepath) as file_content:
headers = {'Content-Type': 'application/gzip'}
response = requests.post(upload_url, file_content, params={'name': filename}, auth=auth,
headers=headers, verify=False)

View File

@@ -1,19 +1,28 @@
machine:
node:
version:
0.10.22
0.10.24
python:
version:
2.7.3
dependencies:
pre:
- make deps
- pip install requests
- pip install -r dev_requirements.txt
- pip install -r requirements.txt
cache_directories:
- rd_ui/node_modules/
- rd_ui/app/bower_components/
test:
override:
- make test
post:
- make pack
- make pack
deployment:
github:
branch: master
commands:
- make upload
notify:
webhooks:
- url: https://webhooks.gitter.im/e/895d09c3165a0913ac2f

3
dev_requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
nose==1.3.0
coverage==3.7.1
mock==1.0.1

169
manage.py Executable file
View File

@@ -0,0 +1,169 @@
#!/usr/bin/env python
"""
CLI to manage redash.
"""
import datetime
from flask.ext.script import Manager, prompt_pass
from redash import settings, models, __version__
from redash.wsgi import app
from redash.import_export import import_manager
manager = Manager(app)
database_manager = Manager(help="Manages the database (create/drop tables).")
users_manager = Manager(help="Users management commands.")
data_sources_manager = Manager(help="Data sources management commands.")
@manager.command
def version():
"""Displays re:dash version."""
print __version__
@manager.command
def runworkers():
"""Prints deprecation warning."""
print "** This command is deprecated. Please use Celery's CLI to control the workers. **"
@manager.shell
def make_shell_context():
from redash.models import db
return dict(app=app, db=db, models=models)
@manager.command
def check_settings():
from types import ModuleType
for name in dir(settings):
item = getattr(settings, name)
if not callable(item) and not name.startswith("__") and not isinstance(item, ModuleType):
print "{} = {}".format(name, item)
@manager.command
def import_events(events_file):
import json
from collections import Counter
count = Counter()
with open(events_file) as f:
for line in f:
try:
event = json.loads(line)
user = event.pop('user_id')
action = event.pop('action')
object_type = event.pop('object_type')
object_id = event.pop('object_id', None)
if object_id == 'dashboard' and object_type == 'dashboard':
count['bad dashboard id'] += 1
continue
created_at = datetime.datetime.utcfromtimestamp(event.pop('timestamp'))
additional_properties = json.dumps(event)
models.Event.create(user=user, action=action, object_type=object_type, object_id=object_id,
additional_properties=additional_properties, created_at=created_at)
count['imported'] += 1
except Exception as ex:
print "Failed importing line:"
print line
print ex.message
count[ex.message] += 1
count['failed'] += 1
models.db.close_db(None)
for k, v in count.iteritems():
print k
print v
@database_manager.command
def create_tables():
"""Creates the database tables."""
from redash.models import create_db, init_db
create_db(True, False)
init_db()
@database_manager.command
def drop_tables():
"""Drop the database tables."""
from redash.models import create_db
create_db(False, True)
@users_manager.option('email', help="User's email")
@users_manager.option('name', help="User's full name")
@users_manager.option('--admin', dest='is_admin', action="store_true", default=False, help="set user as admin")
@users_manager.option('--google', dest='google_auth', action="store_true", default=False, help="user uses Google Auth to login")
@users_manager.option('--password', dest='password', default=None, help="Password for users who don't use Google Auth (leave blank for prompt).")
@users_manager.option('--groups', dest='groups', default=models.User.DEFAULT_GROUPS, help="Comma seperated list of groups (leave blank for default).")
def create(email, name, groups, is_admin=False, google_auth=False, password=None):
print "Creating user (%s, %s)..." % (email, name)
print "Admin: %r" % is_admin
print "Login with Google Auth: %r\n" % google_auth
if isinstance(groups, basestring):
groups= groups.split(',')
groups.remove('') # in case it was empty string
if is_admin:
groups += ['admin']
user = models.User(email=email, name=name, groups=groups)
if not google_auth:
password = password or prompt_pass("Password")
user.hash_password(password)
try:
user.save()
except Exception, e:
print "Failed creating user: %s" % e.message
@users_manager.option('email', help="email address of user to delete")
def delete(email):
deleted_count = models.User.delete().where(models.User.email == email).execute()
print "Deleted %d users." % deleted_count
@data_sources_manager.command
def import_from_settings(name=None):
"""Import data source from settings (env variables)."""
name = name or "Default"
data_source = models.DataSource.create(name=name,
type=settings.CONNECTION_ADAPTER,
options=settings.CONNECTION_STRING)
print "Imported data source from settings (id={}).".format(data_source.id)
@data_sources_manager.command
def list():
"""List currently configured data sources"""
for ds in models.DataSource.select():
print "Name: {}\nType: {}\nOptions: {}".format(ds.name, ds.type, ds.options)
@data_sources_manager.command
def new(name, type, options):
"""Create new data source"""
# TODO: validate it's a valid type and in the future, validate the options.
print "Creating {} data source ({}) with options:\n{}".format(type, name, options)
data_source = models.DataSource.create(name=name,
type=type,
options=options)
print "Id: {}".format(data_source.id)
manager.add_command("database", database_manager)
manager.add_command("users", users_manager)
manager.add_command("import", import_manager)
manager.add_command("ds", data_sources_manager)
if __name__ == '__main__':
manager.run()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1 +0,0 @@

View File

@@ -1,59 +0,0 @@
"""
CLI to start the workers.
TODO: move API server startup here.
"""
import atfork
atfork.monkeypatch_os_fork_functions()
import atfork.stdlib_fixer
atfork.stdlib_fixer.fix_logging_module()
import argparse
import logging
import urlparse
import redis
import time
import settings
import data
def start_workers(data_manager):
try:
old_workers = data_manager.redis_connection.smembers('workers')
data_manager.redis_connection.delete('workers')
logging.info("Cleaning old workers: %s", old_workers)
data_manager.start_workers(settings.WORKERS_COUNT, settings.CONNECTION_STRING)
logging.info("Workers started.")
while True:
try:
data_manager.refresh_queries()
except Exception as e:
logging.error("Something went wrong with refreshing queries...")
logging.exception(e)
time.sleep(60)
except KeyboardInterrupt:
logging.warning("Exiting; waiting for threads")
data_manager.stop_workers()
if __name__ == '__main__':
channel = logging.StreamHandler()
logging.getLogger().addHandler(channel)
logging.getLogger().setLevel(settings.LOG_LEVEL)
parser = argparse.ArgumentParser()
parser.add_argument("command")
args = parser.parse_args()
url = urlparse.urlparse(settings.REDIS_URL)
redis_connection = redis.StrictRedis(host=url.hostname, port=url.port, db=0, password=url.password)
data_manager = data.Manager(redis_connection, settings.INTERNAL_DB_CONNECTION_STRING, settings.MAX_CONNECTIONS)
if args.command == "worker":
start_workers(data_manager)
else:
print "Unknown command"

View File

@@ -1,4 +0,0 @@
from manager import Manager
from worker import Job
import models
import utils

View File

@@ -1,192 +0,0 @@
"""
Data manager. Used to manage and coordinate execution of queries.
"""
import collections
from contextlib import contextmanager
import json
import logging
import psycopg2
import qr
import redis
import time
import worker
import settings
from utils import gen_query_hash
class QueryResult(collections.namedtuple('QueryData', 'id query data runtime retrieved_at query_hash')):
def to_dict(self, parse_data=False):
d = self._asdict()
if parse_data and d['data']:
d['data'] = json.loads(d['data'])
return d
class Manager(object):
def __init__(self, redis_connection, db_connection_string, db_max_connections):
self.redis_connection = redis_connection
self.workers = []
self.db_connection_string = db_connection_string
self.queue = qr.PriorityQueue("jobs", **self.redis_connection.connection_pool.connection_kwargs)
self.max_retries = 5
self.status = {
'last_refresh_at': 0,
'started_at': time.time()
}
self._save_status()
# TODO: Use our Django Models
def get_query_result_by_id(self, query_result_id):
with self.db_transaction() as cursor:
sql = "SELECT id, query, data, runtime, retrieved_at, query_hash FROM query_results " \
"WHERE id=%s LIMIT 1"
cursor.execute(sql, (query_result_id,))
query_result = cursor.fetchone()
if query_result:
query_result = QueryResult(*query_result)
return query_result
def get_query_result(self, query, ttl=0):
query_hash = gen_query_hash(query)
with self.db_transaction() as cursor:
sql = "SELECT id, query, data, runtime, retrieved_at, query_hash FROM query_results " \
"WHERE query_hash=%s " \
"AND retrieved_at < now() at time zone 'utc' - interval '%s second'" \
"ORDER BY retrieved_at DESC LIMIT 1"
cursor.execute(sql, (query_hash, psycopg2.extensions.AsIs(ttl)))
query_result = cursor.fetchone()
if query_result:
query_result = QueryResult(*query_result)
return query_result
def add_job(self, query, priority):
query_hash = gen_query_hash(query)
logging.info("[Manager][%s] Inserting job with priority=%s", query_hash, priority)
try_count = 0
job = None
while try_count < self.max_retries:
try_count += 1
pipe = self.redis_connection.pipeline()
try:
pipe.watch('query_hash_job:%s' % query_hash)
job_id = pipe.get('query_hash_job:%s' % query_hash)
if job_id:
logging.info("[Manager][%s] Found existing job: %s", query_hash, job_id)
job = worker.Job.load(self.redis_connection, job_id)
else:
job = worker.Job(self.redis_connection, query, priority)
pipe.multi()
job.save(pipe)
logging.info("[Manager][%s] Created new job: %s", query_hash, job.id)
self.queue.push(job.id, job.priority)
break
except redis.WatchError:
continue
if not job:
logging.error("[Manager][%s] Failed adding job for query.", query_hash)
return job
def refresh_queries(self):
sql = """SELECT queries.query, queries.ttl, retrieved_at
FROM (SELECT query, min(ttl) as ttl FROM queries WHERE ttl > 0 GROUP by query) queries
JOIN (SELECT query, max(retrieved_at) as retrieved_at
FROM query_results
GROUP BY query) query_results on query_results.query=queries.query
WHERE queries.ttl > 0
AND query_results.retrieved_at + ttl * interval '1 second' < now() at time zone 'utc';"""
self.status['last_refresh_at'] = time.time()
self._save_status()
logging.info("Refreshing queries...")
queries = self.run_query(sql)
for query, ttl, retrieved_at in queries:
self.add_job(query, worker.Job.LOW_PRIORITY)
logging.info("Done refreshing queries... %d" % len(queries))
def store_query_result(self, query, data, run_time, retrieved_at):
query_result_id = None
query_hash = gen_query_hash(query)
sql = "INSERT INTO query_results (query_hash, query, data, runtime, retrieved_at) " \
"VALUES (%s, %s, %s, %s, %s) RETURNING id"
with self.db_transaction() as cursor:
cursor.execute(sql, (query_hash, query, data, run_time, retrieved_at))
if cursor.rowcount == 1:
query_result_id = cursor.fetchone()[0]
logging.info("[Manager][%s] Inserted query data; id=%s", query_hash, query_result_id)
sql = "UPDATE queries SET latest_query_data_id=%s WHERE query_hash=%s"
cursor.execute(sql, (query_result_id, query_hash))
logging.info("[Manager][%s] Updated %s queries.", query_hash, cursor.rowcount)
else:
logging.error("[Manager][%s] Failed inserting query data.", query_hash)
return query_result_id
def run_query(self, *args):
sql = args[0]
logging.debug("running query: %s %s", sql, args[1:])
with self.db_transaction() as cursor:
cursor.execute(sql, args[1:])
if cursor.description:
data = list(cursor)
else:
data = cursor.rowcount
return data
def start_workers(self, workers_count, connection_string):
if self.workers:
return self.workers
if settings.CONNECTION_ADAPTER == "mysql":
import query_runner_mysql
runner = query_runner_mysql.mysql(connection_string)
else:
import query_runner
runner = query_runner.redshift(connection_string)
redis_connection_params = self.redis_connection.connection_pool.connection_kwargs
self.workers = [worker.Worker(self, redis_connection_params, runner)
for _ in range(workers_count)]
for w in self.workers:
w.start()
return self.workers
def stop_workers(self):
for w in self.workers:
w.continue_working = False
w.join()
@contextmanager
def db_transaction(self):
connection = psycopg2.connect(self.db_connection_string)
cursor = connection.cursor()
try:
yield cursor
except:
connection.rollback()
raise
else:
connection.commit()
finally:
connection.close()
def _save_status(self):
self.redis_connection.hmset('manager:status', self.status)

View File

@@ -1,210 +0,0 @@
"""
Django ORM based models to describe the data model of re:dash.
"""
import hashlib
import json
import time
from django.db import models
from django.template.defaultfilters import slugify
import utils
class QueryResult(models.Model):
id = models.AutoField(primary_key=True)
query_hash = models.CharField(max_length=32)
query = models.TextField()
data = models.TextField()
runtime = models.FloatField()
retrieved_at = models.DateTimeField()
class Meta:
app_label = 'redash'
db_table = 'query_results'
def to_dict(self):
return {
'id': self.id,
'query_hash': self.query_hash,
'query': self.query,
'data': json.loads(self.data),
'runtime': self.runtime,
'retrieved_at': self.retrieved_at
}
def __unicode__(self):
return u"%d | %s | %s" % (self.id, self.query_hash, self.retrieved_at)
class Query(models.Model):
id = models.AutoField(primary_key=True)
latest_query_data = models.ForeignKey(QueryResult)
name = models.CharField(max_length=255)
description = models.CharField(max_length=4096)
query = models.TextField()
query_hash = models.CharField(max_length=32)
api_key = models.CharField(max_length=40)
ttl = models.IntegerField()
user = models.CharField(max_length=360)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
app_label = 'redash'
db_table = 'queries'
def to_dict(self, with_result=True, with_stats=False,
with_visualizations=False):
d = {
'id': self.id,
'latest_query_data_id': self.latest_query_data_id,
'name': self.name,
'description': self.description,
'query': self.query,
'query_hash': self.query_hash,
'ttl': self.ttl,
'user': self.user,
'api_key': self.api_key,
'created_at': self.created_at,
}
if with_stats:
d['avg_runtime'] = self.avg_runtime
d['min_runtime'] = self.min_runtime
d['max_runtime'] = self.max_runtime
d['last_retrieved_at'] = self.last_retrieved_at
d['times_retrieved'] = self.times_retrieved
if with_result and self.latest_query_data_id:
d['latest_query_data'] = self.latest_query_data.to_dict()
if with_visualizations:
d['visualizations'] = [vis.to_dict(with_query=False)
for vis in self.visualizations.all()]
return d
@classmethod
def all_queries(cls):
query = """SELECT queries.*, query_stats.*
FROM queries
LEFT OUTER JOIN
(SELECT qu.query_hash,
count(0) AS "times_retrieved",
avg(runtime) AS "avg_runtime",
min(runtime) AS "min_runtime",
max(runtime) AS "max_runtime",
max(retrieved_at) AS "last_retrieved_at"
FROM queries qu
JOIN query_results qr ON qu.query_hash=qr.query_hash
GROUP BY qu.query_hash) query_stats ON query_stats.query_hash = queries.query_hash
"""
return cls.objects.raw(query)
def save(self, *args, **kwargs):
self.query_hash = utils.gen_query_hash(self.query)
self._set_api_key()
super(Query, self).save(*args, **kwargs)
def _set_api_key(self):
if not self.api_key:
self.api_key = hashlib.sha1(
u''.join([str(time.time()), self.query, self.user, self.name])).hexdigest()
def __unicode__(self):
return unicode(self.id)
class Dashboard(models.Model):
id = models.AutoField(primary_key=True)
slug = models.CharField(max_length=140)
name = models.CharField(max_length=100)
user = models.CharField(max_length=360)
layout = models.TextField()
is_archived = models.BooleanField(default=False)
class Meta:
app_label = 'redash'
db_table = 'dashboards'
def to_dict(self, with_widgets=False):
layout = json.loads(self.layout)
if with_widgets:
widgets = {w.id: w.to_dict() for w in self.widgets.all()}
widgets_layout = map(lambda row: map(lambda widget_id: widgets.get(widget_id, None), row), layout)
else:
widgets_layout = None
return {
'id': self.id,
'slug': self.slug,
'name': self.name,
'user': self.user,
'layout': layout,
'widgets': widgets_layout
}
def save(self, *args, **kwargs):
# TODO: make sure slug is unique
if not self.slug:
self.slug = slugify(self.name)
super(Dashboard, self).save(*args, **kwargs)
def __unicode__(self):
return u"%s=%s" % (self.id, self.name)
class Visualization(models.Model):
id = models.AutoField(primary_key=True)
type = models.CharField(max_length=100)
query = models.ForeignKey(Query, related_name='visualizations')
name = models.CharField(max_length=255)
description = models.CharField(max_length=4096)
options = models.TextField()
class Meta:
app_label = 'redash'
db_table = 'visualizations'
def to_dict(self, with_query=True):
d = {
'id': self.id,
'type': self.type,
'name': self.name,
'description': self.description,
'options': json.loads(self.options),
}
if with_query:
d['query'] = self.query.to_dict()
return d
def __unicode__(self):
return u"%s=>%s" % (self.id, self.query_id)
class Widget(models.Model):
id = models.AutoField(primary_key=True)
type = models.CharField(max_length=100)
query = models.ForeignKey(Query, related_name='widgets')
visualization = models.ForeignKey(Visualization, related_name='widgets')
width = models.IntegerField()
options = models.TextField()
dashboard = models.ForeignKey(Dashboard, related_name='widgets')
class Meta:
app_label = 'redash'
db_table = 'widgets'
def to_dict(self):
return {
'id': self.id,
'type': self.type,
'width': self.width,
'options': json.loads(self.options),
'visualization': self.visualization.to_dict(),
'dashboard_id': self.dashboard_id
}
def __unicode__(self):
return u"%s=>%s" % (self.id, self.dashboard_id)

View File

@@ -1,67 +0,0 @@
"""
QueryRunner is the function that the workers use, to execute queries. This is the Redshift
(PostgreSQL in fact) version, but easily we can write another to support additional databases
(MySQL and others).
Because the worker just pass the query, this can be used with any data store that has some sort of
query language (for example: HiveQL).
"""
import logging
import json
import psycopg2
import sys
import select
from .utils import JSONEncoder
def redshift(connection_string):
def column_friendly_name(column_name):
return column_name
def wait(conn):
while 1:
state = conn.poll()
if state == psycopg2.extensions.POLL_OK:
break
elif state == psycopg2.extensions.POLL_WRITE:
select.select([], [conn.fileno()], [])
elif state == psycopg2.extensions.POLL_READ:
select.select([conn.fileno()], [], [])
else:
raise psycopg2.OperationalError("poll() returned %s" % state)
def query_runner(query):
connection = psycopg2.connect(connection_string, async=True)
wait(connection)
cursor = connection.cursor()
try:
cursor.execute(query)
wait(connection)
column_names = [col.name for col in cursor.description]
rows = [dict(zip(column_names, row)) for row in cursor]
columns = [{'name': col.name,
'friendly_name': column_friendly_name(col.name),
'type': None} for col in cursor.description]
data = {'columns': columns, 'rows': rows}
json_data = json.dumps(data, cls=JSONEncoder)
error = None
cursor.close()
except psycopg2.DatabaseError as e:
json_data = None
error = e.message
except KeyboardInterrupt:
connection.cancel()
error = "Query cancelled by user."
json_data = None
except Exception as e:
raise sys.exc_info()[1], None, sys.exc_info()[2]
finally:
connection.close()
return json_data, error
return query_runner

View File

@@ -1,55 +0,0 @@
BEGIN;
CREATE TABLE "query_results" (
"id" serial NOT NULL PRIMARY KEY,
"query_hash" varchar(32) NOT NULL,
"query" text NOT NULL,
"data" text NOT NULL,
"runtime" double precision NOT NULL,
"retrieved_at" timestamp with time zone NOT NULL
)
;
CREATE TABLE "queries" (
"id" serial NOT NULL PRIMARY KEY,
"latest_query_data_id" integer REFERENCES "query_results" ("id") DEFERRABLE INITIALLY DEFERRED,
"name" varchar(255) NOT NULL,
"description" varchar(4096),
"query" text NOT NULL,
"query_hash" varchar(32) NOT NULL,
"api_key" varchar(40),
"ttl" integer NOT NULL,
"user" varchar(360) NOT NULL,
"created_at" timestamp with time zone NOT NULL
)
;
CREATE TABLE "dashboards" (
"id" serial NOT NULL PRIMARY KEY,
"slug" varchar(140) NOT NULL,
"name" varchar(100) NOT NULL,
"user" varchar(360) NOT NULL,
"layout" text NOT NULL,
"is_archived" boolean NOT NULL
)
;
CREATE TABLE "visualizations" (
"id" serial NOT NULL PRIMARY KEY,
"type" varchar(100) NOT NULL,
"query_id" integer NOT NULL REFERENCES "queries" ("id") DEFERRABLE INITIALLY DEFERRED,
"name" varchar(255) NOT NULL,
"description" varchar(4096),
"options" text NOT NULL
)
;
CREATE TABLE "widgets" (
"id" serial NOT NULL PRIMARY KEY,
"type" varchar(100) NOT NULL,
"width" integer NOT NULL,
"options" text NOT NULL,
"visualization_id" integer NOT NULL REFERENCES "visualizations" ("id") DEFERRABLE INITIALLY DEFERRED,
"dashboard_id" integer NOT NULL REFERENCES "dashboards" ("id") DEFERRABLE INITIALLY DEFERRED
)
;
CREATE INDEX "queries_latest_query_data_id" ON "queries" ("latest_query_data_id");
CREATE INDEX "widgets_query_id" ON "widgets" ("query_id");
CREATE INDEX "widgets_dashboard_id" ON "widgets" ("dashboard_id");
COMMIT;

View File

@@ -1,71 +0,0 @@
import cStringIO
import csv
import codecs
import decimal
import datetime
import json
import re
import hashlib
COMMENTS_REGEX = re.compile("/\*.*?\*/")
def gen_query_hash(sql):
"""Returns hash of the given query after stripping all comments, line breaks and multiple
spaces, and lower casing all text.
TODO: possible issue - the following queries will get the same id:
1. SELECT 1 FROM table WHERE column='Value';
2. SELECT 1 FROM table where column='value';
"""
sql = COMMENTS_REGEX.sub("", sql)
sql = "".join(sql.split()).lower()
return hashlib.md5(sql.encode('utf-8')).hexdigest()
class JSONEncoder(json.JSONEncoder):
"""Custom JSON encoding class, to handle Decimal and datetime.date instances.
"""
def default(self, o):
if isinstance(o, decimal.Decimal):
return float(o)
if isinstance(o, datetime.date):
return o.isoformat()
super(JSONEncoder, self).default(o)
class UnicodeWriter:
"""
A CSV writer which will write rows to CSV file "f",
which is encoded in the given encoding.
"""
def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds):
# Redirect output to a queue
self.queue = cStringIO.StringIO()
self.writer = csv.writer(self.queue, dialect=dialect, **kwds)
self.stream = f
self.encoder = codecs.getincrementalencoder(encoding)()
def _encode_utf8(self, val):
if isinstance(val, (unicode, str)):
return val.encode('utf-8')
return val
def writerow(self, row):
self.writer.writerow([self._encode_utf8(s) for s in row])
# Fetch UTF-8 output from the queue ...
data = self.queue.getvalue()
data = data.decode("utf-8")
# ... and reencode it into the target encoding
data = self.encoder.encode(data)
# write to the target stream
self.stream.write(data)
# empty queue
self.queue.truncate(0)
def writerows(self, rows):
for row in rows:
self.writerow(row)

View File

@@ -1,255 +0,0 @@
"""
Worker implementation to execute incoming queries.
"""
import json
import logging
import os
import threading
import uuid
import datetime
import time
import signal
import setproctitle
import redis
from utils import gen_query_hash
class Job(object):
HIGH_PRIORITY = 1
LOW_PRIORITY = 2
WAITING = 1
PROCESSING = 2
DONE = 3
FAILED = 4
def __init__(self, redis_connection, query, priority,
job_id=None,
wait_time=None, query_time=None,
updated_at=None, status=None, error=None, query_result_id=None,
process_id=0):
self.redis_connection = redis_connection
self.query = query
self.priority = priority
self.query_hash = gen_query_hash(self.query)
self.query_result_id = query_result_id
if process_id == 'None':
self.process_id = None
else:
self.process_id = int(process_id)
if job_id is None:
self.id = str(uuid.uuid1())
self.new_job = True
self.wait_time = 0
self.query_time = 0
self.error = None
self.updated_at = time.time() # job_dict.get('updated_at', time.time())
self.status = self.WAITING # int(job_dict.get('status', self.WAITING))
else:
self.id = job_id
self.new_job = False
self.error = error
self.wait_time = wait_time
self.query_time = query_time
self.updated_at = updated_at
self.status = status
def to_dict(self):
return {
'query': self.query,
'priority': self.priority,
'id': self.id,
'wait_time': self.wait_time,
'query_time': self.query_time,
'updated_at': self.updated_at,
'status': self.status,
'error': self.error,
'query_result_id': self.query_result_id,
'process_id': self.process_id
}
@staticmethod
def _redis_key(job_id):
return 'job:%s' % job_id
def cancel(self):
# TODO: Race condition:
# it's possible that it will be picked up by worker while processing the cancel order
if self.is_finished():
return
if self.status == self.PROCESSING:
os.kill(self.process_id, signal.SIGINT)
else:
self.done(None, "Interrupted/Cancelled while running.")
def save(self, pipe=None):
if not pipe:
pipe = self.redis_connection.pipeline()
if self.new_job:
pipe.set('query_hash_job:%s' % self.query_hash, self.id)
if self.is_finished():
pipe.delete('query_hash_job:%s' % self.query_hash)
pipe.sadd('jobs_set', self.id)
pipe.hmset(self._redis_key(self.id), self.to_dict())
pipe.publish(self._redis_key(self.id), json.dumps(self.to_dict()))
pipe.execute()
def processing(self, process_id):
self.status = self.PROCESSING
self.process_id = process_id
self.wait_time = time.time() - self.updated_at
self.updated_at = time.time()
self.save()
def is_finished(self):
return self.status in (self.FAILED, self.DONE)
def done(self, query_result_id, error):
if error:
self.status = self.FAILED
else:
self.status = self.DONE
self.query_result_id = query_result_id
self.error = error
self.query_time = time.time() - self.updated_at
self.updated_at = time.time()
self.save()
def __str__(self):
return "<Job:%s,priority:%d,status:%d>" % (self.id, self.priority, self.status)
@classmethod
def _load(cls, redis_connection, job_id):
return redis_connection.hgetall(cls._redis_key(job_id))
@classmethod
def load(cls, redis_connection, job_id):
job_dict = cls._load(redis_connection, job_id)
job = None
if job_dict:
job = Job(redis_connection, job_id=job_dict['id'], query=job_dict['query'].decode('utf-8'),
priority=int(job_dict['priority']), updated_at=float(job_dict['updated_at']),
status=int(job_dict['status']), wait_time=float(job_dict['wait_time']),
query_time=float(job_dict['query_time']), error=job_dict['error'],
query_result_id=job_dict['query_result_id'],
process_id=job_dict['process_id'])
return job
class Worker(threading.Thread):
def __init__(self, manager, redis_connection_params, query_runner, sleep_time=0.1):
self.manager = manager
self.redis_connection_params = {k: v for k, v in redis_connection_params.iteritems()
if k in ('host', 'db', 'password', 'port')}
self.continue_working = True
self.query_runner = query_runner
self.sleep_time = sleep_time
self.child_pid = None
self.worker_id = uuid.uuid1()
self.status = {
'jobs_count': 0,
'cancelled_jobs_count': 0,
'done_jobs_count': 0,
'updated_at': time.time(),
'started_at': time.time()
}
self._save_status()
self.manager.redis_connection.sadd('workers', self._key)
super(Worker, self).__init__(name="Worker-%s" % self.worker_id)
def set_title(self, title=None):
base_title = "redash worker:%s" % self.worker_id
if title:
full_title = "%s - %s" % (base_title, title)
else:
full_title = base_title
setproctitle.setproctitle(full_title)
def run(self):
logging.info("[%s] started.", self.name)
while self.continue_working:
job_id = self.manager.queue.pop()
if job_id:
self._update_status('jobs_count')
logging.info("[%s] Processing %s", self.name, job_id)
self._fork_and_process(job_id)
if self.child_pid == 0:
return
else:
time.sleep(self.sleep_time)
def _update_status(self, counter):
self.status['updated_at'] = time.time()
self.status[counter] += 1
self._save_status()
@property
def _key(self):
return 'worker:%s' % self.worker_id
def _save_status(self):
self.manager.redis_connection.hmset(self._key, self.status)
def _fork_and_process(self, job_id):
self.child_pid = os.fork()
if self.child_pid == 0:
self.set_title("processing %s" % job_id)
self._process(job_id)
else:
logging.info("[%s] Waiting for pid: %d", self.name, self.child_pid)
_, status = os.waitpid(self.child_pid, 0)
self._update_status('done_jobs_count')
if status > 0:
job = Job.load(self.manager.redis_connection, job_id)
if not job.is_finished():
self._update_status('cancelled_jobs_count')
logging.info("[%s] process interrupted and job %s hasn't finished; registering interruption in job",
self.name, job_id)
job.done(None, "Interrupted/Cancelled while running.")
logging.info("[%s] Finished Processing %s (pid: %d status: %d)",
self.name, job_id, self.child_pid, status)
def _process(self, job_id):
redis_connection = redis.StrictRedis(**self.redis_connection_params)
job = Job.load(redis_connection, job_id)
if job.is_finished():
logging.warning("[%s][%s] tried to process finished job.", self.name, job)
return
pid = os.getpid()
job.processing(pid)
logging.info("[%s][%s] running query...", self.name, job.id)
start_time = time.time()
self.set_title("running query %s" % job_id)
annotated_query = "/* Pid: %s, Job Id: %s, Query hash: %s, Priority: %s */ %s" % \
(pid, job.id, job.query_hash, job.priority, job.query)
# TODO: here's the part that needs to be forked, not all of the worker process...
data, error = self.query_runner(annotated_query)
run_time = time.time() - start_time
logging.info("[%s][%s] query finished... data length=%s, error=%s",
self.name, job.id, data and len(data), error)
# TODO: it is possible that storing the data will fail, and we will need to retry
# while we already marked the job as done
query_result_id = None
if not error:
self.set_title("storing results %s" % job_id)
query_result_id = self.manager.store_query_result(job.query, data, run_time,
datetime.datetime.utcnow())
self.set_title("marking job as done %s" % job_id)
job.done(query_result_id, error)

View File

@@ -1,50 +0,0 @@
import json
import settings
from data.models import *
# first run:
# CREATE TABLE "visualizations" (
# "id" serial NOT NULL PRIMARY KEY,
# "type" varchar(100) NOT NULL,
# "query_id" integer NOT NULL REFERENCES "queries" ("id") DEFERRABLE INITIALLY DEFERRED,
# "name" varchar(255) NOT NULL,
# "description" varchar(4096),
# "options" text NOT NULL
# )
# ;
# ALTER TABLE widgets ADD COLUMN "visualization_id" integer REFERENCES "visualizations" ("id") DEFERRABLE INITIALLY DEFERRED;
if __name__ == '__main__':
print 'migrating Widgets -> Visualizations ...'
for query in Query.objects.filter(name__icontains="cohort"):
vis = Visualization(query=query, name=query.name,
description=query.description,
type="COHORT", options="{}")
vis.save()
for widget in Widget.objects.all():
print 'processing widget %d' % widget.id
query = widget.query
vis_type = widget.type.upper()
vis = query.visualizations.filter(type=vis_type)
if vis:
print 'found'
widget.visualization = vis[0]
widget.save()
else:
options = json.loads(widget.options)
vis_options = {"series": options} if options else {}
vis_options = json.dumps(vis_options)
vis = Visualization(query=query, name=query.name,
description=query.description,
type=vis_type, options=vis_options)
vis.save()
widget.visualization = vis
widget.save()

View File

@@ -1,10 +0,0 @@
psycopg2==2.5.1
redis==2.7.5
tornado==3.0.2
sqlparse==0.1.8
Django==1.5.4
django-db-pool==0.0.10
qr==0.6.0
python-dateutil==2.1
setproctitle==1.1.8
atfork==0.1.2

View File

@@ -1,381 +0,0 @@
"""
Tornado based API implementation for re:dash.
Also at the moment the Tornado server is used to serve the static assets (and the Angular.js app),
but this is only due to configuration issues and temporary.
Usage:
python server.py [--port=8888] [--debug] [--static=..]
port - port to listen to
debug - enable debug mode (extensive logging, restart on code change)
static - static assets path
If static option isn't specified it will be taken from settings.py.
"""
import csv
import hashlib
import json
import numbers
import os
import urlparse
import logging
import cStringIO
import datetime
import dateutil.parser
import redis
import sqlparse
import tornado.ioloop
import tornado.web
import tornado.auth
import tornado.options
import settings
import time
from data import utils
import data
class BaseHandler(tornado.web.RequestHandler):
def initialize(self):
self.data_manager = self.application.settings.get('data_manager', None)
self.redis_connection = self.application.settings['redis_connection']
def get_current_user(self):
user = self.get_secure_cookie("user")
return user
def write_json(self, response, encode=True):
if encode:
response = json.dumps(response, cls=utils.JSONEncoder)
self.set_header("Content-Type", "application/json; charset=UTF-8")
self.write(response)
class BaseAuthenticatedHandler(BaseHandler):
@tornado.web.authenticated
def prepare(self):
pass
class PingHandler(tornado.web.RequestHandler):
def get(self):
self.write("PONG")
class GoogleLoginHandler(tornado.web.RequestHandler,
tornado.auth.GoogleMixin):
@tornado.web.asynchronous
@tornado.gen.coroutine
def get(self):
if self.get_argument("openid.mode", None):
user = yield self.get_authenticated_user()
if user['email'] in settings.ALLOWED_USERS or user['email'].endswith("@%s" % settings.GOOGLE_APPS_DOMAIN):
logging.info("Authenticated: %s", user['email'])
self.set_secure_cookie("user", user['email'])
self.redirect("/")
else:
logging.error("Failed logging in with: %s", user)
self.authenticate_redirect()
else:
self.authenticate_redirect()
class MainHandler(BaseAuthenticatedHandler):
def get(self, *args):
email_md5 = hashlib.md5(self.current_user.lower()).hexdigest()
gravatar_url = "https://www.gravatar.com/avatar/%s?s=40" % email_md5
user = {
'gravatar_url': gravatar_url,
'is_admin': self.current_user in settings.ADMINS,
'name': self.current_user
}
self.render("index.html", user=json.dumps(user), analytics=settings.ANALYTICS)
class QueryFormatHandler(BaseAuthenticatedHandler):
def post(self):
arguments = json.loads(self.request.body)
query = arguments.get("query", "")
self.write(sqlparse.format(query, reindent=True, keyword_case='upper'))
class StatusHandler(BaseAuthenticatedHandler):
def get(self):
status = {}
info = self.redis_connection.info()
status['redis_used_memory'] = info['used_memory_human']
status['queries_count'] = data.models.Query.objects.count()
status['query_results_count'] = data.models.QueryResult.objects.count()
status['dashboards_count'] = data.models.Dashboard.objects.count()
status['widgets_count'] = data.models.Widget.objects.count()
status['workers'] = [self.redis_connection.hgetall(w)
for w in self.redis_connection.smembers('workers')]
manager_status = self.redis_connection.hgetall('manager:status')
status['manager'] = manager_status
status['manager']['queue_size'] = self.redis_connection.zcard('jobs')
self.write_json(status)
class WidgetsHandler(BaseAuthenticatedHandler):
def post(self, widget_id=None):
widget_properties = json.loads(self.request.body)
widget_properties['options'] = json.dumps(widget_properties['options'])
widget = data.models.Widget(**widget_properties)
widget.save()
layout = json.loads(widget.dashboard.layout)
new_row = True
if len(layout) == 0 or widget.width == 2:
layout.append([widget.id])
elif len(layout[-1]) == 1:
neighbour_widget = data.models.Widget.objects.get(pk=layout[-1][0])
if neighbour_widget.width == 1:
layout[-1].append(widget.id)
new_row = False
else:
layout.append([widget.id])
else:
layout.append([widget.id])
widget.dashboard.layout = json.dumps(layout)
widget.dashboard.save()
self.write_json({'widget': widget.to_dict(), 'layout': layout, 'new_row': new_row})
def delete(self, widget_id):
widget_id = int(widget_id)
widget = data.models.Widget.objects.get(pk=widget_id)
# TODO: reposition existing ones
layout = json.loads(widget.dashboard.layout)
layout = map(lambda row: filter(lambda w: w != widget_id, row), layout)
layout = filter(lambda row: len(row) > 0, layout)
widget.dashboard.layout = json.dumps(layout)
widget.dashboard.save()
widget.delete()
class DashboardHandler(BaseAuthenticatedHandler):
def get(self, dashboard_slug=None):
if dashboard_slug:
dashboard = data.models.Dashboard.objects.prefetch_related('widgets__visualization__query__latest_query_data').get(slug=dashboard_slug)
self.write_json(dashboard.to_dict(with_widgets=True))
else:
dashboards = [d.to_dict() for d in
data.models.Dashboard.objects.filter(is_archived=False)]
self.write_json(dashboards)
def post(self, dashboard_id):
if dashboard_id:
dashboard_properties = json.loads(self.request.body)
dashboard = data.models.Dashboard.objects.get(pk=dashboard_id)
dashboard.layout = dashboard_properties['layout']
dashboard.name = dashboard_properties['name']
dashboard.save()
self.write_json(dashboard.to_dict(with_widgets=True))
else:
dashboard_properties = json.loads(self.request.body)
dashboard = data.models.Dashboard(name=dashboard_properties['name'],
user=self.current_user,
layout='[]')
dashboard.save()
self.write_json(dashboard.to_dict())
def delete(self, dashboard_slug):
dashboard = data.models.Dashboard.objects.get(slug=dashboard_slug)
dashboard.is_archived = True
dashboard.save()
class QueriesHandler(BaseAuthenticatedHandler):
def post(self, id=None):
query_def = json.loads(self.request.body)
if 'created_at' in query_def:
query_def['created_at'] = dateutil.parser.parse(query_def['created_at'])
query_def.pop('latest_query_data', None)
query_def.pop('visualizations', None)
if id:
query = data.models.Query(**query_def)
fields = query_def.keys()
fields.remove('id')
query.save(update_fields=fields)
else:
query_def['user'] = self.current_user
query = data.models.Query(**query_def)
query.save()
self.write_json(query.to_dict(with_result=False))
def get(self, id=None):
if id:
q = data.models.Query.objects.get(pk=id)
if q:
self.write_json(q.to_dict(with_visualizations=True))
else:
self.send_error(404)
else:
self.write_json([q.to_dict(with_result=False, with_stats=True) for q in data.models.Query.all_queries()])
class QueryResultsHandler(BaseAuthenticatedHandler):
def get(self, query_result_id):
query_result = self.data_manager.get_query_result_by_id(query_result_id)
if query_result:
self.write_json({'query_result': query_result.to_dict(parse_data=True)})
else:
self.send_error(404)
def post(self, _):
params = json.loads(self.request.body)
if params['ttl'] == 0:
query_result = None
else:
query_result = self.data_manager.get_query_result(params['query'], int(params['ttl']))
if query_result:
self.write_json({'query_result': query_result.to_dict(parse_data=True)})
else:
job = self.data_manager.add_job(params['query'], data.Job.HIGH_PRIORITY)
self.write({'job': job.to_dict()})
class VisualizationHandler(BaseAuthenticatedHandler):
def get(self, id):
pass
def post(self, id=None):
kwargs = json.loads(self.request.body)
kwargs['options'] = json.dumps(kwargs['options'])
if id:
vis = data.models.Visualization(**kwargs)
fields = kwargs.keys()
fields.remove('id')
vis.save(update_fields=fields)
else:
vis = data.models.Visualization(**kwargs)
vis.save()
self.write_json(vis.to_dict(with_query=False))
def delete(self, id):
vis = data.models.Visualization.objects.get(pk=id)
vis.delete()
class CsvQueryResultsHandler(BaseAuthenticatedHandler):
def get_current_user(self):
user = super(CsvQueryResultsHandler, self).get_current_user()
if not user:
api_key = self.get_argument("api_key", None)
query = data.models.Query.objects.get(pk=self.path_args[0])
if query.api_key and query.api_key == api_key:
user = "API-Key=%s" % api_key
return user
def get(self, query_id, result_id=None):
if not result_id:
query = data.models.Query.objects.get(pk=query_id)
if query:
result_id = query.latest_query_data_id
query_result = result_id and self.data_manager.get_query_result_by_id(result_id)
if query_result:
self.set_header("Content-Type", "text/csv; charset=UTF-8")
s = cStringIO.StringIO()
query_data = json.loads(query_result.data)
writer = csv.DictWriter(s, fieldnames=[col['name'] for col in query_data['columns']])
writer.writer = utils.UnicodeWriter(s)
writer.writeheader()
for row in query_data['rows']:
for k, v in row.iteritems():
if isinstance(v, numbers.Number) and (v > 1000 * 1000 * 1000 * 100):
row[k] = datetime.datetime.fromtimestamp(v/1000.0)
writer.writerow(row)
self.write(s.getvalue())
else:
self.send_error(404)
class JobsHandler(BaseAuthenticatedHandler):
def get(self, job_id=None):
if job_id:
# TODO: if finished, include the query result
job = data.Job.load(self.data_manager.redis_connection, job_id)
self.write({'job': job.to_dict()})
else:
raise NotImplemented
def delete(self, job_id):
job = data.Job.load(self.data_manager.redis_connection, job_id)
job.cancel()
def get_application(static_path, is_debug, redis_connection, data_manager):
return tornado.web.Application([(r"/", MainHandler),
(r"/ping", PingHandler),
(r"/api/queries/([0-9]*)/results(?:/([0-9]*))?.csv", CsvQueryResultsHandler),
(r"/api/queries/format", QueryFormatHandler),
(r"/api/queries(?:/([0-9]*))?", QueriesHandler),
(r"/api/query_results(?:/([0-9]*))?", QueryResultsHandler),
(r"/api/jobs/(.*)", JobsHandler),
(r"/api/visualizations(?:/([0-9]*))?", VisualizationHandler),
(r"/api/widgets(?:/([0-9]*))?", WidgetsHandler),
(r"/api/dashboards(?:/(.*))?", DashboardHandler),
(r"/admin/(.*)", MainHandler),
(r"/dashboard/(.*)", MainHandler),
(r"/queries(.*)", MainHandler),
(r"/login", GoogleLoginHandler),
(r"/status.json", StatusHandler),
(r"/(.*)", tornado.web.StaticFileHandler,
{"path": static_path})],
template_path=static_path,
static_path=static_path,
debug=is_debug,
login_url="/login",
cookie_secret=settings.COOKIE_SECRET,
redis_connection=redis_connection,
data_manager=data_manager)
if __name__ == '__main__':
tornado.options.define("port", default=8888, type=int)
tornado.options.define("debug", default=False, type=bool)
tornado.options.define("static", default=settings.STATIC_ASSETS_PATH, type=str)
tornado.options.parse_command_line()
root_path = os.path.dirname(__file__)
static_path = os.path.abspath(os.path.join(root_path, tornado.options.options.static))
url = urlparse.urlparse(settings.REDIS_URL)
redis_connection = redis.StrictRedis(host=url.hostname, port=url.port, db=0, password=url.password)
data_manager = data.Manager(redis_connection, settings.INTERNAL_DB_CONNECTION_STRING,
settings.MAX_CONNECTIONS)
logging.info("re:dash web server stating on port: %d...", tornado.options.options.port)
logging.info("UI assets path: %s...", static_path)
application = get_application(static_path, tornado.options.options.debug,
redis_connection, data_manager)
application.listen(tornado.options.options.port)
tornado.ioloop.IOLoop.instance().start()

View File

@@ -1,41 +0,0 @@
"""
Example settings module. You should make your own copy as settings.py and enter the real settings.
"""
import django.conf
REDIS_URL = "redis://localhost:6379"
# Either "pg" or "mysql"
CONNECTION_ADAPTER = "mysql"
# Connection string for the database that is used to run queries against
# -- example mysql CONNECTION_STRING = "Server=;User=;Pwd=;Database="
# -- example pg CONNECTION_STRING = "user= password= host= port=5439 dbname="
CONNECTION_STRING = "user= password= host= port=5439 dbname="
# Connection string for the operational databases (where we store the queries, results, etc)
INTERNAL_DB_CONNECTION_STRING = "dbname=postgres"
# Google Apps domain to allow access from; any user with email in this Google Apps will be allowed
# access
GOOGLE_APPS_DOMAIN = ""
# Email addresses of specific users not from the above set Google Apps Domain, that you want to
# allow access to re:dash
ALLOWED_USERS = []
# Email addresses of admin users
ADMINS = []
STATIC_ASSETS_PATH = "../rd_ui/dist/"
WORKERS_COUNT = 2
MAX_CONNECTIONS = 3
COOKIE_SECRET = "c292a0a3aa32397cdb050e233733900f"
LOG_LEVEL = "INFO"
ANALYTICS = ""
# Configuration of the operational database for the Django models
django.conf.settings.configure(DATABASES = { 'default': {
'ENGINE': 'dbpool.db.backends.postgresql_psycopg2',
'OPTIONS': {'MAX_CONNS': 10, 'MIN_CONNS': 1},
'NAME': 'postgres',
'USER': '',
'PASSWORD': '',
'HOST': '',
'PORT': '',
},}, TIME_ZONE = 'UTC')

View File

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

View File

@@ -1,10 +1,5 @@
// Generated on 2013-08-25 using generator-angular 0.4.0
// Generated on 2014-07-30 using generator-angular 0.9.2
'use strict';
var LIVERELOAD_PORT = 35729;
var lrSnippet = require('connect-livereload')({ port: LIVERELOAD_PORT });
var mountFolder = function (connect, dir) {
return connect.static(require('path').resolve(dir));
};
// # Globbing
// for performance reasons we're only matching one level down:
@@ -13,48 +8,148 @@ var mountFolder = function (connect, dir) {
// 'test/spec/**/*.js'
module.exports = function (grunt) {
// Load grunt tasks automatically
require('load-grunt-tasks')(grunt);
// Time how long tasks take. Can help when optimizing build times
require('time-grunt')(grunt);
// configurable paths
var yeomanConfig = {
app: 'app',
// Configurable paths for the application
var appConfig = {
app: require('./bower.json').appPath || 'app',
dist: 'dist'
};
try {
yeomanConfig.app = require('./bower.json').appPath || yeomanConfig.app;
} catch (e) {}
// Define the configuration for all the tasks
grunt.initConfig({
yeoman: yeomanConfig,
// Project settings
yeoman: appConfig,
// Watches files for changes and runs tasks based on the changed files
watch: {
coffee: {
files: ['<%= yeoman.app %>/scripts/{,*/}*.coffee'],
tasks: ['coffee:dist']
bower: {
files: ['bower.json'],
tasks: ['wiredep']
},
coffeeTest: {
files: ['test/spec/{,*/}*.coffee'],
tasks: ['coffee:test']
js: {
files: ['<%= yeoman.app %>/scripts/{,*/}*.js'],
tasks: ['newer:jshint:all'],
options: {
livereload: '<%= connect.options.livereload %>'
}
},
jsTest: {
files: ['test/spec/{,*/}*.js'],
tasks: ['newer:jshint:test', 'karma']
},
styles: {
files: ['<%= yeoman.app %>/styles/{,*/}*.css'],
tasks: ['copy:styles', 'autoprefixer']
tasks: ['newer:copy:styles', 'autoprefixer']
},
gruntfile: {
files: ['Gruntfile.js']
},
livereload: {
options: {
livereload: LIVERELOAD_PORT
livereload: '<%= connect.options.livereload %>'
},
files: [
'<%= yeoman.app %>/{,*/}*.html',
'.tmp/styles/{,*/}*.css',
'{.tmp,<%= yeoman.app %>}/scripts/{,*/}*.js',
'<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}'
]
}
},
// The actual grunt server settings
connect: {
options: {
port: 9000,
// Change this to '0.0.0.0' to access the server from outside.
hostname: 'localhost',
livereload: 35729
},
livereload: {
options: {
open: true,
middleware: function (connect) {
return [
connect.static('.tmp'),
connect().use(
'/bower_components',
connect.static('./bower_components')
),
connect.static(appConfig.app)
];
}
}
},
test: {
options: {
port: 9001,
middleware: function (connect) {
return [
connect.static('.tmp'),
connect.static('test'),
connect().use(
'/bower_components',
connect.static('./bower_components')
),
connect.static(appConfig.app)
];
}
}
},
dist: {
options: {
open: true,
base: '<%= yeoman.dist %>'
}
}
},
// Make sure code styles are up to par and there are no obvious mistakes
jshint: {
options: {
jshintrc: '.jshintrc',
reporter: require('jshint-stylish')
},
all: {
src: [
'Gruntfile.js',
'<%= yeoman.app %>/scripts/{,*/}*.js'
]
},
test: {
options: {
jshintrc: 'test/.jshintrc'
},
src: ['test/spec/{,*/}*.js']
}
},
// Empties folders to start fresh
clean: {
dist: {
files: [{
dot: true,
src: [
'.tmp',
'<%= yeoman.dist %>/{,*/}*',
'!<%= yeoman.dist %>/.git*'
]
}]
},
server: '.tmp'
},
// Add vendor prefixed styles
autoprefixer: {
options: ['last 1 version'],
options: {
browsers: ['last 1 version']
},
dist: {
files: [{
expand: true,
@@ -64,134 +159,95 @@ module.exports = function (grunt) {
}]
}
},
connect: {
// Automatically inject Bower components into the app
wiredep: {
options: {
port: 9000,
// Change this to '0.0.0.0' to access the server from outside.
hostname: 'localhost'
cwd: '<%= yeoman.app %>'
},
livereload: {
options: {
middleware: function (connect) {
return [
lrSnippet,
mountFolder(connect, '.tmp'),
mountFolder(connect, yeomanConfig.app)
];
}
}
},
test: {
options: {
middleware: function (connect) {
return [
mountFolder(connect, '.tmp'),
mountFolder(connect, 'test')
];
}
}
},
dist: {
options: {
middleware: function (connect) {
return [
mountFolder(connect, yeomanConfig.dist)
];
}
}
app: {
src: ['<%= yeoman.app %>/index.html'],
ignorePath: /\.\.\//
}
},
open: {
server: {
url: 'http://localhost:<%= connect.options.port %>'
}
},
clean: {
dist: {
files: [{
dot: true,
src: [
'.tmp',
'<%= yeoman.dist %>/*',
'!<%= yeoman.dist %>/.git*'
]
}]
},
server: '.tmp'
},
jshint: {
options: {
jshintrc: '.jshintrc'
},
all: [
'Gruntfile.js',
'<%= yeoman.app %>/scripts/{,*/}*.js'
]
},
coffee: {
options: {
sourceMap: true,
sourceRoot: ''
},
dist: {
files: [{
expand: true,
cwd: '<%= yeoman.app %>/scripts',
src: '{,*/}*.coffee',
dest: '.tmp/scripts',
ext: '.js'
}]
},
test: {
files: [{
expand: true,
cwd: 'test/spec',
src: '{,*/}*.coffee',
dest: '.tmp/spec',
ext: '.js'
}]
}
},
// not used since Uglify task does concat,
// but still available if needed
/*concat: {
dist: {}
},*/
rev: {
dist: {
files: {
src: [
'<%= yeoman.dist %>/scripts/{,*/}*.js',
'<%= yeoman.dist %>/styles/{,*/}*.css',
'<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}',
'<%= yeoman.dist %>/styles/fonts/*'
]
}
// Renames files for browser caching purposes
filerev: {
dist: {
src: [
'<%= yeoman.dist %>/scripts/{,*/}*.js',
'<%= yeoman.dist %>/styles/{,*/}*.css',
'<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}',
'<%= yeoman.dist %>/styles/fonts/*'
]
}
},
// Reads HTML for usemin blocks to enable smart builds that automatically
// concat, minify and revision files. Creates configurations in memory so
// additional tasks can operate on them
useminPrepare: {
html: '<%= yeoman.app %>/index.html',
html: ['<%= yeoman.app %>/index.html', '<%= yeoman.app %>/login.html'],
options: {
dest: '<%= yeoman.dist %>'
dest: '<%= yeoman.dist %>',
flow: {
html: {
steps: {
js: ['concat', 'uglifyjs'],
css: ['cssmin']
},
post: {}
}
}
}
},
// Performs rewrites based on filerev and the useminPrepare configuration
usemin: {
html: ['<%= yeoman.dist %>/{,*/}*.html'],
css: ['<%= yeoman.dist %>/styles/{,*/}*.css'],
options: {
dirs: ['<%= yeoman.dist %>']
assetsDirs: ['<%= yeoman.dist %>','<%= yeoman.dist %>/images']
}
},
// The following *-min tasks will produce minified files in the dist folder
// By default, your `index.html`'s <!-- Usemin block --> will take care of
// minification. These next options are pre-configured if you do not wish
// to use the Usemin blocks.
// cssmin: {
// dist: {
// files: {
// '<%= yeoman.dist %>/styles/main.css': [
// '.tmp/styles/{,*/}*.css'
// ]
// }
// }
// },
// uglify: {
// dist: {
// files: {
// '<%= yeoman.dist %>/scripts/scripts.js': [
// '<%= yeoman.dist %>/scripts/scripts.js'
// ]
// }
// }
// },
// concat: {
// dist: {}
// },
imagemin: {
dist: {
files: [{
expand: true,
cwd: '<%= yeoman.app %>/images',
src: '{,*/}*.{png,jpg,jpeg}',
src: '{,*/}*.{png,jpg,jpeg,gif}',
dest: '<%= yeoman.dist %>/images'
}]
}
},
svgmin: {
dist: {
files: [{
@@ -202,41 +258,47 @@ module.exports = function (grunt) {
}]
}
},
cssmin: {
// By default, your `index.html` <!-- Usemin Block --> will take care of
// minification. This option is pre-configured if you do not wish to use
// Usemin blocks.
// dist: {
// files: {
// '<%= yeoman.dist %>/styles/main.css': [
// '.tmp/styles/{,*/}*.css',
// '<%= yeoman.app %>/styles/{,*/}*.css'
// ]
// }
// }
},
htmlmin: {
dist: {
options: {
/*removeCommentsFromCDATA: true,
// https://github.com/yeoman/grunt-usemin/issues/44
//collapseWhitespace: true,
collapseWhitespace: true,
conservativeCollapse: true,
collapseBooleanAttributes: true,
removeAttributeQuotes: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeOptionalTags: true*/
removeCommentsFromCDATA: true,
removeOptionalTags: true
},
files: [{
expand: true,
cwd: '<%= yeoman.app %>',
src: ['*.html', 'views/*.html'],
cwd: '<%= yeoman.dist %>',
src: ['*.html', 'views/{,*/}*.html'],
dest: '<%= yeoman.dist %>'
}]
}
},
// Put files not handled in other tasks here
// ngmin tries to make the code safe for minification automatically by
// using the Angular long form for dependency injection. It doesn't work on
// things like resolve or inject so those have to be done manually.
ngmin: {
dist: {
files: [{
expand: true,
cwd: '.tmp/concat/scripts',
src: '*.js',
dest: '.tmp/concat/scripts'
}]
}
},
// Replace Google CDN references
cdnify: {
dist: {
html: ['<%= yeoman.dist %>/*.html']
}
},
// Copies remaining files to places other tasks can use
copy: {
dist: {
files: [{
@@ -247,17 +309,21 @@ module.exports = function (grunt) {
src: [
'*.{ico,png,txt}',
'.htaccess',
'bower_components/**/*',
'images/{,*/}*.{gif,webp}',
'*.html',
'views/{,*/}*.html',
'images/{,*/}*.{webp}',
'fonts/*'
]
}, {
expand: true,
cwd: '.tmp/images',
dest: '<%= yeoman.dist %>/images',
src: [
'generated/*'
]
src: ['generated/*']
}, {
expand: true,
cwd: 'bower_components/bootstrap/dist',
src: 'fonts/*',
dest: '<%= yeoman.dist %>'
}]
},
styles: {
@@ -267,70 +333,52 @@ module.exports = function (grunt) {
src: '{,*/}*.css'
}
},
// Run some tasks in parallel to speed up the build process
concurrent: {
server: [
'coffee:dist',
'copy:styles'
],
test: [
'coffee',
'copy:styles'
],
dist: [
'coffee',
'copy:styles',
'imagemin',
'svgmin',
'htmlmin'
'svgmin'
]
},
// Test settings
karma: {
unit: {
configFile: 'karma.conf.js',
configFile: 'test/karma.conf.js',
singleRun: true
}
},
cdnify: {
dist: {
html: ['<%= yeoman.dist %>/*.html']
}
},
ngmin: {
dist: {
files: [{
expand: true,
cwd: '<%= yeoman.dist %>/scripts',
src: '*.js',
dest: '<%= yeoman.dist %>/scripts'
}]
}
},
uglify: {
dist: {
files: {
'<%= yeoman.dist %>/scripts/scripts.js': [
'<%= yeoman.dist %>/scripts/scripts.js'
]
}
}
}
});
grunt.registerTask('server', function (target) {
grunt.registerTask('serve', 'Compile then start a connect web server', function (target) {
if (target === 'dist') {
return grunt.task.run(['build', 'open', 'connect:dist:keepalive']);
return grunt.task.run(['build', 'connect:dist:keepalive']);
}
grunt.task.run([
'clean:server',
'wiredep',
'concurrent:server',
'autoprefixer',
'connect:livereload',
'open',
'watch'
]);
});
grunt.registerTask('server', 'DEPRECATED TASK. Use the "serve" task instead', function (target) {
grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.');
grunt.task.run(['serve:' + target]);
});
grunt.registerTask('test', [
'clean:server',
'concurrent:test',
@@ -341,21 +389,23 @@ module.exports = function (grunt) {
grunt.registerTask('build', [
'clean:dist',
'wiredep',
'useminPrepare',
'concurrent:dist',
'autoprefixer',
'concat',
'ngmin',
'copy:dist',
'cdnify',
'ngmin',
'cssmin',
'uglify',
'rev',
'usemin'
'filerev',
'usemin',
'htmlmin'
]);
grunt.registerTask('default', [
'jshint',
'newer:jshint',
'test',
'build'
]);

BIN
rd_ui/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -4,7 +4,7 @@
<!--[if IE 8]> <html class="no-js lt-ie9" ng-app="redash" ng-controller='MainCtrl'> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js" ng-app="redash" ng-controller='MainCtrl'> <!--<![endif]-->
<head>
<title ng-bind="'re:dash | ' + pageTitle"></title>
<title ng-bind="'{{name}} | ' + pageTitle"></title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
@@ -12,8 +12,10 @@
<link rel="stylesheet" href="/bower_components/bootstrap/dist/css/bootstrap.css">
<link rel="stylesheet" href="/bower_components/codemirror/lib/codemirror.css">
<link rel="stylesheet" href="/bower_components/gridster/dist/jquery.gridster.css">
<link rel="stylesheet" href="/bower_components/pivottable/examples/pivot.css">
<link rel="stylesheet" href="/bower_components/pivottable/dist/pivot.css">
<link rel="stylesheet" href="/bower_components/cornelius/src/cornelius.css">
<link rel="stylesheet" href="/bower_components/select2/select2.css">
<link rel="stylesheet" href="/bower_components/pace/themes/pace-theme-minimal.css">
<link rel="stylesheet" href="/styles/redash.css">
<!-- endbuild -->
</head>
@@ -29,12 +31,13 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/"><strong>re:dash</strong></a>
<a class="navbar-brand" href="/"><strong>{{name}}</strong></a>
</div>
{% raw %}
<div class="collapse navbar-collapse navbar-ex1-collapse">
<ul class="nav navbar-nav">
<li class="active" ng-show="pageTitle"><a class="page-title" ng-bind="pageTitle"></a></li>
<li class="dropdown">
<li class="dropdown" ng-show="groupedDashboards.length > 0 || otherDashboards.length > 0 || currentUser.hasPermission('create_dashboard')">
<a href="#" class="dropdown-toggle" data-toggle="dropdown"><span class="glyphicon glyphicon-th-large"></span> <b class="caret"></b></a>
<ul class="dropdown-menu">
<span ng-repeat="(name, group) in groupedDashboards">
@@ -42,32 +45,36 @@
<a href="#" ng-bind="name"></a>
<ul class="dropdown-menu">
<li ng-repeat="dashboard in group" role="presentation">
<a role="menu-item" ng-href="/dashboard/{{!dashboard.slug}}" ng-bind="dashboard.name"></a>
<a role="menu-item" ng-href="/dashboard/{{dashboard.slug}}" ng-bind="dashboard.name"></a>
</li>
</ul>
</li>
</span>
<li ng-repeat="dashboard in otherDashboards">
<a role="menu-item" ng-href="/dashboard/{{!dashboard.slug}}" ng-bind="dashboard.name"></a>
<a role="menu-item" ng-href="/dashboard/{{dashboard.slug}}" ng-bind="dashboard.name"></a>
</li>
<li class="divider"></li>
<li><a data-toggle="modal" href="#new_dashboard_dialog">New Dashboard</a></li>
<li class="divider" ng-show="currentUser.hasPermission('create_dashboard') && (groupedDashboards.length > 0 || otherDashboards.length > 0)"></li>
<li><a data-toggle="modal" href="#new_dashboard_dialog" ng-show="currentUser.hasPermission('create_dashboard')">New Dashboard</a></li>
</ul>
</li>
<li class="dropdown">
<li class="dropdown" ng-show="currentUser.hasPermission('view_query')">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Queries <b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="/queries/new">New Query</a></li>
<li ng-show="currentUser.hasPermission('create_query')"><a href="/queries/new">New Query</a></li>
<li><a href="/queries">Queries</a></li>
</ul>
</li>
</ul>
<ul class="nav navbar-nav navbar-right">
<p class="navbar-text avatar">
<img ng-src="{{!currentUser.gravatar_url}}" class="img-circle" alt="{{!currentUser.name}}" width="40" height="40"/>
<p class="navbar-text avatar" ng-show="currentUser.id" ng-cloak>
<img ng-src="{{currentUser.gravatar_url}}" class="img-circle" alt="{{currentUser.name}}"/>
<a target="_self" href="/logout" id="logout" title="Logout">
<span class="glyphicon glyphicon-log-out"></span>
</a>
</p>
</ul>
</div>
{% endraw %}
</div>
</nav>
@@ -95,38 +102,61 @@
<script src="/bower_components/angular-ui-codemirror/ui-codemirror.js"></script>
<script src="/bower_components/highcharts/highcharts.js"></script>
<script src="/bower_components/highcharts/modules/exporting.js"></script>
<script src="/scripts/ng-highchart.js"></script>
<script src="/scripts/smart-table.js"></script>
<script src="/scripts/ui-bootstrap-tpls-0.5.0.min.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/examples/pivot.js"></script>
<script src="/bower_components/pivottable/dist/pivot.js"></script>
<script src="/bower_components/cornelius/src/cornelius.js"></script>
<script src="/bower_components/mousetrap/mousetrap.js"></script>
<script src="/bower_components/mousetrap/plugins/global-bind/mousetrap-global-bind.js"></script>
<script src="/bower_components/select2/select2.js"></script>
<script src="/bower_components/angular-ui-select2/src/select2.js"></script>
<script src="/bower_components/underscore.string/lib/underscore.string.js"></script>
<script src="/bower_components/marked/lib/marked.js"></script>
<script src="/scripts/ng_highchart.js"></script>
<script src="/scripts/ng_smart_table.js"></script>
<script src="/scripts/ui-bootstrap-tpls-0.5.0.min.js"></script>
<script src="/bower_components/bucky/bucky.js"></script>
<script src="/bower_components/pace/pace.js"></script>
<!-- endbuild -->
<!-- build:js({.tmp,app}) /scripts/scripts.js -->
<script src="/scripts/app.js"></script>
<script src="/scripts/controllers.js"></script>
<script src="/scripts/admin_controllers.js"></script>
<script src="/scripts/directives.js"></script>
<script src="/scripts/services.js"></script>
<script src="/scripts/filters.js"></script>
<script src="/scripts/services/services.js"></script>
<script src="/scripts/services/resources.js"></script>
<script src="/scripts/services/notifications.js"></script>
<script src="/scripts/services/dashboards.js"></script>
<script src="/scripts/query_fiddle/renderers.js"></script>
<script src="/scripts/controllers/controllers.js"></script>
<script src="/scripts/controllers/dashboard.js"></script>
<script src="/scripts/controllers/admin_controllers.js"></script>
<script src="/scripts/controllers/query_view.js"></script>
<script src="/scripts/controllers/query_source.js"></script>
<script src="/scripts/visualizations/base.js"></script>
<script src="/scripts/visualizations/chart.js"></script>
<script src="/scripts/visualizations/cohort.js"></script>
<script src="/scripts/visualizations/table.js"></script>
<script src="/scripts/visualizations/pivot.js"></script>
<script src="/scripts/directives/directives.js"></script>
<script src="/scripts/directives/query_directives.js"></script>
<script src="/scripts/directives/dashboard_directives.js"></script>
<script src="/scripts/filters.js"></script>
<!-- endbuild -->
<script>
var currentUser = {% raw user %};
// TODO: move currentUser & features to be an Angular service
var featureFlags = {{ features|safe }};
var currentUser = {{ user|safe }};
currentUser.canEdit = function(object) {
return object.user && (object.user.indexOf(currentUser.name) != -1);
var user_id = object.user_id || (object.user && object.user.id);
return user_id && (user_id == currentUser.id);
};
{% raw analytics %}
currentUser.hasPermission = function(permission) {
return this.permissions.indexOf(permission) != -1;
}
{{ analytics|safe }}
</script>
</body>
</html>
</html>

85
rd_ui/app/login.html Normal file
View File

@@ -0,0 +1,85 @@
<!DOCTYPE html>
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
<head>
<title>{{name}} Login</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- build:css /styles/main_login.css -->
<link rel="stylesheet" href="/bower_components/bootstrap/dist/css/bootstrap.css">
<link rel="stylesheet" href="/styles/redash.css">
<link rel="stylesheet" href="/styles/login.css">
<!-- endbuild -->
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse"
data-target=".navbar-ex1-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/"><strong>{{name}}</strong></a>
</div>
</div>
</nav>
<div class="container">
<div class="row">
<div class="main">
<form role="form" method="post" name="login">
<div class="form-group">
<label for="inputUsernameEmail">Username or email</label>
<input type="text" class="form-control" id="inputUsernameEmail" name="username" value="{{username}}">
</div>
<div class="form-group">
<!--<a class="pull-right" href="#">Forgot password?</a>-->
<label for="inputPassword">Password</label>
<input type="password" class="form-control" id="inputPassword" name="password">
</div>
<div class="checkbox pull-right">
<label>
<input type="checkbox" name="remember">
Remember me </label>
</div>
<button type="submit" class="btn btn btn-primary">
Log In
</button>
</form>
{% if show_google_openid %}
<div class="login-or">
<hr class="hr-or">
<span class="span-or">or</span>
</div>
<div class="row">
<div class="col-xs-6 col-sm-6 col-md-6">
<a href="/google_auth/login?next={{next}}" class="btn btn-lg btn-info btn-block">Google</a>
</div>
</div>
{% endif %}
</div>
</div>
</div>
<script src="/bower_components/jquery/jquery.js"></script>
<script>
{{ analytics|safe }}
</script>
</body>
</html>

View File

@@ -1,23 +0,0 @@
(function () {
var AdminStatusCtrl = function ($scope, $http, $timeout) {
$scope.$parent.pageTitle = "System Status";
var refresh = function () {
$scope.refresh_time = moment().add('minutes', 1);
$http.get('/status.json').success(function (data) {
$scope.workers = data.workers;
delete data.workers;
$scope.manager = data.manager;
delete data.manager;
$scope.status = data;
});
$timeout(refresh, 59 * 1000);
};
refresh();
}
angular.module('redash.admin_controllers', [])
.controller('AdminStatusCtrl', ['$scope', '$http', '$timeout', AdminStatusCtrl])
})();

View File

@@ -5,55 +5,89 @@ angular.module('redash', [
'redash.filters',
'redash.services',
'redash.renderers',
'redash.visualization',
'ui.codemirror',
'highchart',
'ui.select2',
'angular-growl',
'angularMoment',
'ui.bootstrap',
'smartTable.table',
'ngResource',
'ngRoute'
]).config(['$routeProvider', '$locationProvider', '$compileProvider', 'growlProvider',
function($routeProvider, $locationProvider, $compileProvider, growlProvider) {
$compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|http|data):/);
$locationProvider.html5Mode(true);
growlProvider.globalTimeToLive(2000);
$routeProvider.when('/dashboard/:dashboardSlug', {
templateUrl: '/views/dashboard.html',
controller: 'DashboardCtrl'
});
$routeProvider.when('/queries', {
templateUrl: '/views/queries.html',
controller: 'QueriesCtrl',
reloadOnSearch: false
});
$routeProvider.when('/queries/new', {
templateUrl: '/views/queryfiddle.html',
controller: 'QueryFiddleCtrl',
reloadOnSearch: false
});
$routeProvider.when('/queries/:queryId', {
templateUrl: '/views/queryfiddle.html',
controller: 'QueryFiddleCtrl',
reloadOnSearch: false
});
$routeProvider.when('/admin/status', {
templateUrl: '/views/admin_status.html',
controller: 'AdminStatusCtrl'
});
$routeProvider.when('/', {
templateUrl: '/views/index.html',
controller: 'IndexCtrl'
});
$routeProvider.otherwise({
redirectTo: '/'
]).config(['$routeProvider', '$locationProvider', '$compileProvider', 'growlProvider',
function ($routeProvider, $locationProvider, $compileProvider, growlProvider) {
if (featureFlags.clientSideMetrics) {
Bucky.setOptions({
host: '/api/metrics'
});
Highcharts.setOptions({
colors: ["#4572A7", "#AA4643", "#89A54E", "#80699B", "#3D96AE",
"#DB843D", "#92A8CD", "#A47D7C", "#B5CA92"]
});
Bucky.requests.monitor('ajax_requsts');
Bucky.requests.transforms.enable('dashboards', /dashboard\/[\w-]+/ig, '/dashboard');
}
function getQuery(Query, $route) {
var query = Query.get({'id': $route.current.params.queryId });
return query.$promise;
};
$compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|http|data):/);
$locationProvider.html5Mode(true);
growlProvider.globalTimeToLive(2000);
$routeProvider.when('/dashboard/:dashboardSlug', {
templateUrl: '/views/dashboard.html',
controller: 'DashboardCtrl',
reloadOnSearch: false
});
$routeProvider.when('/queries', {
templateUrl: '/views/queries.html',
controller: 'QueriesCtrl',
reloadOnSearch: false
});
$routeProvider.when('/queries/new', {
templateUrl: '/views/query.html',
controller: 'QuerySourceCtrl',
reloadOnSearch: false,
resolve: {
'query': ['Query', function newQuery(Query) {
return Query.newQuery();
}]
}
});
$routeProvider.when('/queries/:queryId', {
templateUrl: '/views/query.html',
controller: 'QueryViewCtrl',
reloadOnSearch: false,
resolve: {
'query': ['Query', '$route', getQuery]
}
});
$routeProvider.when('/queries/:queryId/source', {
templateUrl: '/views/query.html',
controller: 'QuerySourceCtrl',
reloadOnSearch: false,
resolve: {
'query': ['Query', '$route', getQuery]
}
});
$routeProvider.when('/admin/status', {
templateUrl: '/views/admin_status.html',
controller: 'AdminStatusCtrl'
});
$routeProvider.when('/admin/workers', {
templateUrl: '/views/admin_workers.html',
controller: 'AdminWorkersCtrl'
});
$routeProvider.when('/', {
templateUrl: '/views/index.html',
controller: 'IndexCtrl'
});
$routeProvider.otherwise({
redirectTo: '/'
});
}
]);
]);

View File

@@ -1,395 +0,0 @@
(function () {
var DashboardCtrl = function ($scope, $routeParams, $http, Dashboard) {
$scope.dashboard = Dashboard.get({slug: $routeParams.dashboardSlug}, function(dashboard) {
$scope.$parent.pageTitle = dashboard.name;
});
};
var WidgetCtrl = function ($scope, $http, $location, Query) {
$scope.deleteWidget = function() {
if (!confirm('Are you sure you want to remove "' + $scope.widget.visualization.name + '" from the dashboard?')) {
return;
}
$http.delete('/api/widgets/' + $scope.widget.id).success(function() {
$scope.dashboard.widgets = _.map($scope.dashboard.widgets, function(row) {
return _.filter(row, function(widget) {
return widget.id != $scope.widget.id;
})
});
});
};
$scope.open = function(query) {
$location.path('/queries/' + query.id);
}
$scope.query = new Query($scope.widget.visualization.query);
$scope.queryResult = $scope.query.getQueryResult();
$scope.updateTime = (new Date($scope.queryResult.getUpdatedAt())).toISOString();
$scope.nextUpdateTime = moment(new Date(($scope.query.updated_at + $scope.query.ttl + $scope.query.runtime + 300) * 1000)).fromNow();
$scope.updateTime = '';
}
var QueryFiddleCtrl = function ($scope, $window, $location, $routeParams, $http, $location, growl, notifications, Query, Visualization) {
var DEFAULT_TAB = 'table';
var pristineHash = null;
var leavingPageText = "You will lose your changes if you leave";
$scope.dirty = undefined;
$scope.newVisualization = undefined;
$window.onbeforeunload = function(){
if (currentUser.canEdit($scope.query) && $scope.dirty) {
return leavingPageText;
}
}
Mousetrap.bindGlobal("meta+s", function(e) {
e.preventDefault();
if (currentUser.canEdit($scope.query)) {
$scope.saveQuery();
}
});
$scope.$on('$locationChangeStart', function(event, next, current) {
if (next.split("#")[0] == current.split("#")[0]) {
return;
}
if (!currentUser.canEdit($scope.query)) {
return;
}
if($scope.dirty &&
!confirm(leavingPageText + "\n\nAre you sure you want to leave this page?")) {
event.preventDefault();
} else {
Mousetrap.unbind("meta+s");
}
});
$scope.$parent.pageTitle = "Query Fiddle";
$scope.$watch(function() {return $location.hash()}, function(hash) {
$scope.selectedTab = hash || DEFAULT_TAB;
});
$scope.lockButton = function (lock) {
$scope.queryExecuting = lock;
};
$scope.formatQuery = function() {
$scope.editorOptions.readOnly = 'nocursor';
$http.post('/api/queries/format', {'query': $scope.query.query}).success(function(response) {
$scope.query.query = response;
$scope.editorOptions.readOnly = false;
})
}
$scope.saveQuery = function (duplicate, oldId) {
if (!oldId) {
oldId = $scope.query.id;
}
delete $scope.query.latest_query_data;
$scope.query.$save(function (q) {
pristineHash = q.getHash();
$scope.dirty = false;
if (duplicate) {
growl.addInfoMessage("Query duplicated.", {ttl: 2000});
} else{
growl.addSuccessMessage("Query saved.", {ttl: 2000});
}
if (oldId != q.id) {
if (oldId == undefined) {
$location.path($location.path().replace('new', q.id)).replace();
} else {
// TODO: replace this with a safer method
$location.path($location.path().replace(oldId, q.id)).replace();
}
}
}, function(httpResponse) {
growl.addErrorMessage("Query could not be saved");
});
};
$scope.duplicateQuery = function () {
var oldId = $scope.query.id;
$scope.query.id = null;
$scope.query.ttl = -1;
$scope.saveQuery(true, oldId);
};
// Query Editor:
$scope.editorOptions = {
mode: 'text/x-sql',
lineWrapping: true,
lineNumbers: true,
readOnly: false,
matchBrackets: true,
autoCloseBrackets: true
};
$scope.refreshOptions = [
{value: -1, name: 'No Refresh'},
]
_.each(_.range(1, 13), function(i) {
$scope.refreshOptions.push({value: i*3600, name: 'Every ' + i + 'h'});
})
$scope.refreshOptions.push({value: 24*3600, name: 'Every 24h'});
$scope.refreshOptions.push({value: 7*24*3600, name: 'Once a week'});
$scope.$watch('queryResult && queryResult.getError()', function (newError, oldError) {
if (newError == undefined) {
return;
}
if (oldError == undefined && newError != undefined) {
$scope.lockButton(false);
}
});
$scope.$watch('queryResult && queryResult.getData()', function (data, oldData) {
if (!data) {
return;
}
if ($scope.queryResult.getId() == null) {
$scope.dataUri = "";
} else {
$scope.dataUri = '/api/queries/' + $scope.query.id + '/results/' + $scope.queryResult.getId() + '.csv';
$scope.dataFilename = $scope.query.name.replace(" ", "_") + moment($scope.queryResult.getUpdatedAt()).format("_YYYY_MM_DD") + ".csv";
}
});
$scope.$watch("queryResult && queryResult.getStatus()", function (status) {
if (!status) {
return;
}
if (status == "done") {
if ($scope.query.id && $scope.query.latest_query_data_id != $scope.queryResult.getId() &&
$scope.query.query_hash == $scope.queryResult.query_result.query_hash) {
Query.save({'id': $scope.query.id, 'latest_query_data_id': $scope.queryResult.getId()})
}
$scope.query.latest_query_data_id = $scope.queryResult.getId();
notifications.showNotification("re:dash", $scope.query.name + " updated.");
$scope.lockButton(false);
}
});
if ($routeParams.queryId != undefined) {
$scope.query = Query.get({id: $routeParams.queryId}, function(q) {
pristineHash = q.getHash();
$scope.dirty = false;
$scope.queryResult = $scope.query.getQueryResult();
});
} else {
$scope.query = new Query({query: "", name: "New Query", ttl: -1, user: currentUser.name});
$scope.lockButton(false);
}
$scope.$watch('query.name', function() {
$scope.$parent.pageTitle = $scope.query.name;
});
$scope.$watch(function() {
return $scope.query.getHash();
}, function(newHash) {
$scope.dirty = (newHash !== pristineHash);
});
$scope.executeQuery = function() {
$scope.queryResult = $scope.query.getQueryResult(0);
$scope.lockButton(true);
$scope.cancelling = false;
};
$scope.cancelExecution = function() {
$scope.cancelling = true;
$scope.queryResult.cancelExecution();
};
$scope.deleteVisualization = function($e, vis) {
$e.preventDefault();
if (confirm('Are you sure you want to delete ' + vis.name + ' ?')) {
Visualization.delete(vis);
if ($scope.selectedTab == vis.id) {
$scope.selectedTab = DEFAULT_TAB;
}
$scope.query.visualizations =
$scope.query.visualizations.filter(function(v) {
return vis.id !== v.id;
});
}
};
}
var QueriesCtrl = function($scope, $http, $location, $filter, Query) {
$scope.$parent.pageTitle = "All Queries";
$scope.gridConfig = {
isPaginationEnabled: true,
itemsByPage: 50,
maxSize: 8,
isGlobalSearchActivated: true
}
$scope.allQueries = [];
$scope.queries = [];
var dateFormatter = function (value) {
if (!value) return "-";
return value.format("DD/MM/YY HH:mm");
}
var filterQueries = function() {
$scope.queries = _.filter($scope.allQueries, function(query) {
if (!$scope.selectedTab) {
return false;
}
if ($scope.selectedTab.key == 'my') {
return query.user == currentUser.name && query.name != 'New Query';
} else if ($scope.selectedTab.key == 'drafts') {
return query.user == currentUser.name && query.name == 'New Query';
}
return query.name != 'New Query';
});
}
Query.query(function(queries) {
$scope.allQueries = _.map(queries, function(query) {
query.created_at = moment(query.created_at);
query.last_retrieved_at = moment(query.last_retrieved_at);
return query;
});
filterQueries();
});
$scope.gridColumns = [
{
"label": "Name",
"map": "name",
"cellTemplateUrl": "/views/queries_query_name_cell.html"
},
{
'label': 'Created By',
'map': 'user'
},
{
'label': 'Created At',
'map': 'created_at',
'formatFunction': dateFormatter
},
{
'label': 'Runtime (avg)',
'map': 'avg_runtime',
'formatFunction': function(value) {
return $filter('durationHumanize')(value);
}
},
{
'label': 'Runtime (min)',
'map': 'min_runtime',
'formatFunction': function(value) {
return $filter('durationHumanize')(value);
}
},
{
'label': 'Runtime (max)',
'map': 'max_runtime',
'formatFunction': function(value) {
return $filter('durationHumanize')(value);
}
},
{
'label': 'Last Executed At',
'map': 'last_retrieved_at',
'formatFunction': dateFormatter
},
{
'label': 'Times Executed',
'map': 'times_retrieved'
},
{
'label': 'Update Schedule',
'map': 'ttl',
'formatFunction': function(value) {
return $filter('refreshRateHumanize')(value);
}
}
]
$scope.tabs = [{"name": "My Queries", "key": "my"}, {"key": "all", "name": "All Queries"}, {"key": "drafts", "name": "Drafts"}];
$scope.$watch('selectedTab', function(tab) {
if (tab) {
$scope.$parent.pageTitle = tab.name;
}
filterQueries();
});
}
var MainCtrl = function ($scope, Dashboard, notifications) {
$scope.dashboards = [];
$scope.reloadDashboards = function() {
Dashboard.query(function (dashboards) {
$scope.dashboards = _.sortBy(dashboards, "name");
$scope.allDashboards = _.groupBy($scope.dashboards, function(d) {
parts = d.name.split(":");
if (parts.length == 1) {
return "Other";
}
return parts[0];
});
$scope.otherDashboards = $scope.allDashboards['Other'] || [];
$scope.groupedDashboards = _.omit($scope.allDashboards, 'Other');
});
}
$scope.reloadDashboards();
$scope.currentUser = currentUser;
$scope.newDashboard = {
'name': null,
'layout': null
}
$(window).click(function () {
notifications.getPermissions();
});
}
var IndexCtrl = function($scope, Dashboard) {
$scope.$parent.pageTitle = "Home";
$scope.archiveDashboard = function(dashboard) {
if (confirm('Are you sure you want to delete "' + dashboard.name + '" dashboard?')) {
dashboard.$delete(function() {
$scope.$parent.reloadDashboards();
});
}
}
}
angular.module('redash.controllers', [])
.controller('DashboardCtrl', ['$scope', '$routeParams', '$http', 'Dashboard', DashboardCtrl])
.controller('WidgetCtrl', ['$scope', '$http', '$location', 'Query', WidgetCtrl])
.controller('QueriesCtrl', ['$scope', '$http', '$location', '$filter', 'Query', QueriesCtrl])
.controller('QueryFiddleCtrl', ['$scope', '$window', '$location', '$routeParams', '$http', '$location', 'growl', 'notifications', 'Query', 'Visualization', QueryFiddleCtrl])
.controller('IndexCtrl', ['$scope', 'Dashboard', IndexCtrl])
.controller('MainCtrl', ['$scope', 'Dashboard', 'notifications', MainCtrl]);
})();

View File

@@ -0,0 +1,31 @@
(function () {
var AdminStatusCtrl = function ($scope, Events, $http, $timeout) {
Events.record(currentUser, "view", "page", "admin/status");
$scope.$parent.pageTitle = "System Status";
var refresh = function () {
$scope.refresh_time = moment().add('minutes', 1);
$http.get('/status.json').success(function (data) {
$scope.workers = data.workers;
delete data.workers;
$scope.manager = data.manager;
delete data.manager;
$scope.status = data;
});
$timeout(refresh, 59 * 1000);
};
$scope.flowerUrl = featureFlags.flowerUrl;
refresh();
}
var AdminWorkersCtrl = function ($scope, $sce) {
$scope.flowerUrl = $sce.trustAsResourceUrl(featureFlags.flowerUrl);
};
angular.module('redash.admin_controllers', [])
.controller('AdminStatusCtrl', ['$scope', 'Events', '$http', '$timeout', AdminStatusCtrl])
.controller('AdminWorkersCtrl', ['$scope', '$sce', AdminWorkersCtrl])
})();

View File

@@ -0,0 +1,169 @@
(function () {
var QueriesCtrl = function ($scope, $http, $location, $filter, Query) {
$scope.$parent.pageTitle = "All Queries";
$scope.gridConfig = {
isPaginationEnabled: true,
itemsByPage: 50,
maxSize: 8,
isGlobalSearchActivated: true
}
$scope.allQueries = [];
$scope.queries = [];
var dateFormatter = function (value) {
if (!value) return "-";
return value.format("DD/MM/YY HH:mm");
}
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';
});
}
Query.query(function (queries) {
$scope.allQueries = _.map(queries, function (query) {
query.created_at = moment(query.created_at);
query.last_retrieved_at = moment(query.last_retrieved_at);
return query;
});
filterQueries();
});
$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 (avg)',
'map': 'avg_runtime',
'formatFunction': function (value) {
return $filter('durationHumanize')(value);
}
},
{
'label': 'Runtime (min)',
'map': 'min_runtime',
'formatFunction': function (value) {
return $filter('durationHumanize')(value);
}
},
{
'label': 'Runtime (max)',
'map': 'max_runtime',
'formatFunction': function (value) {
return $filter('durationHumanize')(value);
}
},
{
'label': 'Last Executed At',
'map': 'last_retrieved_at',
'formatFunction': dateFormatter
},
{
'label': 'Times Executed',
'map': 'times_retrieved'
},
{
'label': 'Update Schedule',
'map': 'ttl',
'formatFunction': function (value) {
return $filter('refreshRateHumanize')(value);
}
}
]
$scope.tabs = [
{"name": "My Queries", "key": "my"},
{"key": "all", "name": "All Queries"},
{"key": "drafts", "name": "Drafts"}
];
$scope.$watch('selectedTab', function (tab) {
if (tab) {
$scope.$parent.pageTitle = tab.name;
}
filterQueries();
});
}
var MainCtrl = function ($scope, Dashboard, notifications) {
if (featureFlags.clientSideMetrics) {
$scope.$on('$locationChangeSuccess', function(event, newLocation, oldLocation) {
// This will be called once per actual page load.
Bucky.sendPagePerformance();
});
}
$scope.dashboards = [];
$scope.reloadDashboards = function () {
Dashboard.query(function (dashboards) {
$scope.dashboards = _.sortBy(dashboards, "name");
$scope.allDashboards = _.groupBy($scope.dashboards, function (d) {
parts = d.name.split(":");
if (parts.length == 1) {
return "Other";
}
return parts[0];
});
$scope.otherDashboards = $scope.allDashboards['Other'] || [];
$scope.groupedDashboards = _.omit($scope.allDashboards, 'Other');
});
}
$scope.reloadDashboards();
$scope.currentUser = currentUser;
$scope.newDashboard = {
'name': null,
'layout': null
}
$(window).click(function () {
notifications.getPermissions();
});
}
var IndexCtrl = function ($scope, Events, Dashboard) {
Events.record(currentUser, "view", "page", "homepage");
$scope.$parent.pageTitle = "Home";
$scope.archiveDashboard = function (dashboard) {
if (confirm('Are you sure you want to delete "' + dashboard.name + '" dashboard?')) {
Events.record(currentUser, "archive", "dashboard", dashboard.id);
dashboard.$delete(function () {
$scope.$parent.reloadDashboards();
});
}
}
}
angular.module('redash.controllers', [])
.controller('QueriesCtrl', ['$scope', '$http', '$location', '$filter', 'Query', QueriesCtrl])
.controller('IndexCtrl', ['$scope', 'Events', 'Dashboard', IndexCtrl])
.controller('MainCtrl', ['$scope', 'Dashboard', 'notifications', MainCtrl]);
})();

View File

@@ -0,0 +1,143 @@
(function() {
var DashboardCtrl = function($scope, Events, Widget, $routeParams, $http, $timeout, $q, Dashboard) {
$scope.refreshEnabled = false;
$scope.refreshRate = 60;
var loadDashboard = _.throttle(function() {
$scope.dashboard = Dashboard.get({ slug: $routeParams.dashboardSlug }, function (dashboard) {
Events.record(currentUser, "view", "dashboard", dashboard.id);
$scope.$parent.pageTitle = dashboard.name;
var promises = [];
$scope.dashboard.widgets = _.map($scope.dashboard.widgets, function (row) {
return _.map(row, function (widget) {
var w = new Widget(widget);
if (w.visualization && dashboard.dashboard_filters_enabled) {
promises.push(w.getQuery().getQueryResultPromise());
}
return w;
});
});
$q.all(promises).then(function(queryResults) {
var filters = {};
_.each(queryResults, function(queryResult) {
var queryFilters = queryResult.getFilters();
_.each(queryFilters, function (filter) {
if (!_.has(filters, filter.name)) {
// TODO: first object should be a copy, otherwise one of the chart filters behaves different than the others.
filters[filter.name] = filter;
filters[filter.name].originFilters = [];
$scope.$watch(function () { return filter.current }, function (value) {
_.each(filter.originFilters, function (originFilter) {
originFilter.current = value;
});
});
};
// TODO: merge values.
filters[filter.name].originFilters.push(filter);
});
});
if (dashboard.dashboard_filters_enabled) {
$scope.filters = _.values(filters);
}
});
}, function () {
// error...
// try again. we wrap loadDashboard with throttle so it doesn't happen too often.\
// we might want to consider exponential backoff and also move this as a general solution in $http/$resource for
// all AJAX calls.
loadDashboard();
});
}, 1000);
loadDashboard();
var autoRefresh = function() {
if ($scope.refreshEnabled) {
$timeout(function() {
Dashboard.get({
slug: $routeParams.dashboardSlug
}, function(dashboard) {
var newWidgets = _.groupBy(_.flatten(dashboard.widgets), 'id');
_.each($scope.dashboard.widgets, function(row) {
_.each(row, function(widget, i) {
var newWidget = newWidgets[widget.id];
if (newWidget && newWidget[0].visualization.query.latest_query_data_id != widget.visualization.query.latest_query_data_id) {
row[i] = newWidget[0];
}
});
});
autoRefresh();
});
}, $scope.refreshRate);
};
}
$scope.triggerRefresh = function() {
$scope.refreshEnabled = !$scope.refreshEnabled;
Events.record(currentUser, "autorefresh", "dashboard", dashboard.id, {'enable': $scope.refreshEnabled});
if ($scope.refreshEnabled) {
var refreshRate = _.min(_.flatten($scope.dashboard.widgets), function(widget) {
return widget.visualization.query.ttl;
}).visualization.query.ttl;
$scope.refreshRate = _.max([120, refreshRate * 2]) * 1000;
autoRefresh();
}
};
};
var WidgetCtrl = function($scope, Events, Query) {
$scope.deleteWidget = function() {
if (!confirm('Are you sure you want to remove "' + $scope.widget.getName() + '" from the dashboard?')) {
return;
}
Events.record(currentUser, "delete", "widget", $scope.widget.id);
$scope.widget.$delete(function() {
$scope.dashboard.widgets = _.map($scope.dashboard.widgets, function(row) {
return _.filter(row, function(widget) {
return widget.id != undefined;
})
});
});
};
Events.record(currentUser, "view", "widget", $scope.widget.id);
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();
$scope.queryResult = $scope.query.getQueryResult();
$scope.nextUpdateTime = moment(new Date(($scope.query.updated_at + $scope.query.ttl + $scope.query.runtime + 300) * 1000)).fromNow();
$scope.type = 'visualization';
} else {
$scope.type = 'textbox';
}
};
angular.module('redash.controllers')
.controller('DashboardCtrl', ['$scope', 'Events', 'Widget', '$routeParams', '$http', '$timeout', '$q', 'Dashboard', DashboardCtrl])
.controller('WidgetCtrl', ['$scope', 'Events', 'Query', WidgetCtrl])
})();

View File

@@ -0,0 +1,120 @@
(function() {
'use strict';
function QuerySourceCtrl(Events, growl, $controller, $scope, $location, Query, Visualization, KeyboardShortcuts) {
// extends QueryViewCtrl
$controller('QueryViewCtrl', {$scope: $scope});
// TODO:
// This doesn't get inherited. Setting it on this didn't work either (which is weird).
// Obviously it shouldn't be repeated, but we got bigger fish to fry.
var DEFAULT_TAB = 'table';
Events.record(currentUser, 'view_source', 'query', $scope.query.id);
var isNewQuery = !$scope.query.id,
queryText = $scope.query.query,
// ref to QueryViewCtrl.saveQuery
saveQuery = $scope.saveQuery,
shortcuts = {
'meta+s': function () {
if ($scope.canEdit) {
$scope.saveQuery();
}
},
'meta+enter': function () {
$scope.executeQuery();
}
};
$scope.sourceMode = true;
$scope.canEdit = currentUser.canEdit($scope.query);
$scope.isDirty = false;
$scope.newVisualization = undefined;
// @override
Object.defineProperty($scope, 'showDataset', {
get: function() {
return $scope.queryResult && $scope.queryResult.getStatus() == 'done';
}
});
KeyboardShortcuts.bind(shortcuts);
// @override
$scope.saveQuery = function(options, data) {
var savePromise = saveQuery(options, data);
savePromise.then(function(savedQuery) {
queryText = savedQuery.query;
$scope.isDirty = $scope.query.query !== queryText;
if (isNewQuery) {
// redirect to new created query (keep hash)
$location.path(savedQuery.getSourceLink()).replace();
}
});
return savePromise;
};
$scope.duplicateQuery = function() {
Events.record(currentUser, 'fork', 'query', $scope.query.id);
$scope.query.id = null;
$scope.query.ttl = -1;
$scope.saveQuery({
successMessage: 'Query forked',
errorMessage: 'Query could not be forked'
}).then(function redirect(savedQuery) {
// redirect to forked query (clear hash)
$location.url(savedQuery.getSourceLink()).replace()
});
};
$scope.deleteVisualization = function($e, vis) {
$e.preventDefault();
if (confirm('Are you sure you want to delete ' + vis.name + ' ?')) {
Events.record(currentUser, 'delete', 'visualization', vis.id);
Visualization.delete(vis, function() {
if ($scope.selectedTab == vis.id) {
$scope.selectedTab = DEFAULT_TAB;
$location.hash($scope.selectedTab);
}
$scope.query.visualizations =
$scope.query.visualizations.filter(function (v) {
return vis.id !== v.id;
});
}, function () {
growl.addErrorMessage("Error deleting visualization. Maybe it's used in a dashboard?");
});
}
};
$scope.$watch('query.query', function(newQueryText) {
$scope.isDirty = (newQueryText !== queryText);
});
$scope.$on('$destroy', function destroy() {
KeyboardShortcuts.unbind(shortcuts);
});
if (isNewQuery) {
// save new query when creating a visualization
var unbind = $scope.$watch('selectedTab == "add"', function(triggerSave) {
if (triggerSave) {
unbind();
$scope.saveQuery();
}
});
}
}
angular.module('redash.controllers').controller('QuerySourceCtrl', [
'Events', 'growl', '$controller', '$scope', '$location', 'Query',
'Visualization', 'KeyboardShortcuts', QuerySourceCtrl
]);
})();

View File

@@ -0,0 +1,144 @@
(function() {
'use strict';
function QueryViewCtrl($scope, Events, $route, $location, notifications, growl, Query, DataSource) {
var DEFAULT_TAB = 'table';
$scope.query = $route.current.locals.query;
Events.record(currentUser, 'view', 'query', $scope.query.id);
$scope.queryResult = $scope.query.getQueryResult();
$scope.queryExecuting = false;
$scope.isQueryOwner = currentUser.id === $scope.query.user.id;
$scope.canViewSource = currentUser.hasPermission('view_source');
$scope.dataSources = DataSource.get(function(dataSources) {
$scope.query.data_source_id = $scope.query.data_source_id || dataSources[0].id;
});
// in view mode, latest dataset is always visible
// source mode changes this behavior
$scope.showDataset = true;
$scope.lockButton = function(lock) {
$scope.queryExecuting = lock;
};
$scope.saveQuery = function(options, data) {
if (data) {
data.id = $scope.query.id;
} else {
data = _.clone($scope.query);
}
options = _.extend({}, {
successMessage: 'Query saved',
errorMessage: 'Query could not be saved'
}, options);
delete data.latest_query_data;
delete data.queryResult;
return Query.save(data, function() {
growl.addSuccessMessage(options.successMessage);
}, function(httpResponse) {
growl.addErrorMessage(options.errorMessage);
}).$promise;
}
$scope.saveDescription = function() {
Events.record(currentUser, 'edit_description', 'query', $scope.query.id);
$scope.saveQuery(undefined, {'description': $scope.query.description});
};
$scope.saveName = function() {
Events.record(currentUser, 'edit_name', 'query', $scope.query.id);
$scope.saveQuery(undefined, {'name': $scope.query.name});
};
$scope.executeQuery = function() {
$scope.queryResult = $scope.query.getQueryResult(0);
$scope.lockButton(true);
$scope.cancelling = false;
Events.record(currentUser, 'execute', 'query', $scope.query.id);
};
$scope.cancelExecution = function() {
$scope.cancelling = true;
$scope.queryResult.cancelExecution();
Events.record(currentUser, 'cancel_execute', 'query', $scope.query.id);
};
$scope.updateDataSource = function() {
Events.record(currentUser, 'update_data_source', 'query', $scope.query.id);
$scope.query.latest_query_data = null;
$scope.query.latest_query_data_id = null;
if ($scope.query.id) {
Query.save({
'id': $scope.query.id,
'data_source_id': $scope.query.data_source_id,
'latest_query_data_id': null
});
}
$scope.executeQuery();
};
$scope.setVisualizationTab = function (visualization) {
$scope.selectedTab = visualization.id;
$location.hash(visualization.id);
};
$scope.$watch('query.name', function() {
$scope.$parent.pageTitle = $scope.query.name;
});
$scope.$watch('queryResult && queryResult.getData()', function(data, oldData) {
if (!data) {
return;
}
$scope.filters = $scope.queryResult.getFilters();
});
$scope.$watch("queryResult && queryResult.getStatus()", function(status) {
if (!status) {
return;
}
if (status == 'done') {
if ($scope.query.id &&
$scope.query.latest_query_data_id != $scope.queryResult.getId() &&
$scope.query.query_hash == $scope.queryResult.query_result.query_hash) {
Query.save({
'id': $scope.query.id,
'latest_query_data_id': $scope.queryResult.getId()
})
}
$scope.query.latest_query_data_id = $scope.queryResult.getId();
$scope.query.queryResult = $scope.queryResult;
notifications.showNotification("re:dash", $scope.query.name + " updated.");
}
if (status === 'done' || status === 'failed') {
$scope.lockButton(false);
}
});
$scope.$watch(function() {
return $location.hash()
}, function(hash) {
if (hash == 'pivot') {
Events.record(currentUser, 'pivot', 'query', $scope.query && $scope.query.id);
}
$scope.selectedTab = hash || DEFAULT_TAB;
});
};
angular.module('redash.controllers')
.controller('QueryViewCtrl',
['$scope', 'Events', '$route', '$location', 'notifications', 'growl', 'Query', 'DataSource', QueryViewCtrl]);
})();

View File

@@ -1,399 +0,0 @@
(function() {
'use strict';
var directives = angular.module('redash.directives', []);
directives.directive('rdTab', ['$location', function($location) {
return {
restrict: 'E',
scope: {
'id': '@',
'name': '@'
},
transclude: true,
template: '<li class="rd-tab" ng-class="{active: id==selectedTab}"><a href="#{{id}}">{{name}}<span ng-transclude></span></a></li>',
replace: true,
link: function(scope) {
scope.$watch(function(){return scope.$parent.selectedTab}, function(tab) {
scope.selectedTab = tab;
});
}
}
}]);
directives.directive('rdTabs', ['$location', '$rootScope', function($location, $rootScope) {
return {
restrict: 'E',
scope: {
tabsCollection: '=',
selectedTab: '='
},
template: '<ul class="nav nav-tabs"><li ng-class="{active: tab==selectedTab}" ng-repeat="tab in tabsCollection"><a href="#{{tab.key}}">{{tab.name}}</a></li></ul>',
replace: true,
link: function($scope, element, attrs) {
$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);
}
});
}
}
}]);
directives.directive('editVisulatizationForm', ['Visualization', 'growl', function(Visualization, growl) {
return {
restrict: 'E',
templateUrl: '/views/edit_visualization.html',
replace: true,
scope: {
query: '=',
vis: '=?'
},
link: function(scope, element, attrs) {
scope.advancedMode = false;
scope.visTypes = {
'Chart': Visualization.prototype.TYPES.CHART,
'Cohort': Visualization.prototype.TYPES.COHORT,
'Table': Visualization.prototype.TYPES.GRID
};
scope.seriesTypes = {
'Line': Visualization.prototype.SERIES_TYPES.LINE,
'Bar': Visualization.prototype.SERIES_TYPES.BAR,
'Area': Visualization.prototype.SERIES_TYPES.AREA
};
if (!scope.vis) {
// create new visualization
// wait for query to load to populate with defaults
var unwatch = scope.$watch('query', function(q) {
if (q && q.id) {
unwatch();
scope.vis = {
'query_id': q.id,
'type': Visualization.prototype.TYPES.CHART,
'name': q.name,
'description': q.description,
'options': newOptions()
};
}
}, true);
}
function newOptions(chartType) {
if (chartType === Visualization.prototype.TYPES.COHORT) {
// empty config at the moment
return {};
}
// Chart
return {
'series': {
'type': Visualization.prototype.SERIES_TYPES.LINE
}
};
}
scope.toggleAdvancedMode = function() {
scope.advancedMode = !scope.advancedMode;
};
scope.typeChanged = function() {
scope.vis.options = newOptions();
};
scope.submit = function() {
Visualization.save(scope.vis, function success(result) {
growl.addSuccessMessage("Visualization saved");
scope.vis = result;
var visIds = _.pluck(scope.query.visualizations, 'id');
var index = visIds.indexOf(result.id);
if (index > -1) {
scope.query.visualizations[index] = result;
} else {
scope.query.visualizations.push(result);
}
}, function error() {
growl.addErrorMessage("Visualization could not be saved");
});
};
}
}
}]);
directives.directive('editDashboardForm', ['$http', '$location', '$timeout', 'Dashboard', function($http, $location, $timeout, Dashboard) {
return {
restrict: 'E',
scope: {
dashboard: '='
},
templateUrl: '/views/edit_dashboard.html',
replace: true,
link: function($scope, element, attrs) {
var gridster = element.find(".gridster ul").gridster({
widget_margins: [5, 5],
widget_base_dimensions: [260, 100],
min_cols: 2,
max_cols: 2,
serialize_params: function($w, wgd) {
return {
col: wgd.col,
row: wgd.row,
id: $w.data('widget-id')
}
}
}).data('gridster');
var gsItemTemplate = '<li data-widget-id="{id}" class="widget panel panel-default gs-w">' +
'<div class="panel-heading">{name}' +
'</div></li>';
$scope.$watch('dashboard.widgets', function(widgets) {
$timeout(function () {
gridster.remove_all_widgets();
if (widgets && widgets.length) {
var layout = [];
_.each(widgets, function(row, rowIndex) {
_.each(row, function(widget, colIndex) {
layout.push({
id: widget.id,
col: colIndex+1,
row: rowIndex+1,
ySize: 1,
xSize: widget.width,
name: widget.visualization.name
});
});
});
_.each(layout, function(item) {
var el = gsItemTemplate.replace('{id}', item.id).replace('{name}', item.name);
gridster.add_widget(el, item.xSize, item.ySize, item.col, item.row);
});
}
});
}, true);
$scope.saveDashboard = function() {
$scope.saveInProgress = true;
// TODO: we should use the dashboard service here.
if ($scope.dashboard.id) {
var positions = $(element).find('.gridster ul').data('gridster').serialize();
var layout = [];
_.each(_.sortBy(positions, function (pos) {
return pos.row * 10 + pos.col;
}), function (pos) {
var row = pos.row - 1;
var col = pos.col - 1;
layout[row] = layout[row] || [];
if (col > 0 && layout[row][col - 1] == undefined) {
layout[row][col - 1] = pos.id;
} else {
layout[row][col] = pos.id;
}
});
$scope.dashboard.layout = layout;
layout = JSON.stringify(layout);
$http.post('/api/dashboards/' + $scope.dashboard.id, {'name': $scope.dashboard.name, 'layout': layout}).success(function(response) {
$scope.dashboard = new Dashboard(response);
$scope.saveInProgress = false;
$(element).modal('hide');
})
} else {
$http.post('/api/dashboards', {'name': $scope.dashboard.name}).success(function(response) {
$(element).modal('hide');
$location.path('/dashboard/' + response.slug).replace();
})
}
}
}
}
}]);
directives.directive('newWidgetForm', ['$http', 'Query', function($http, Query) {
return {
restrict: 'E',
scope: {
dashboard: '='
},
templateUrl: '/views/new_widget_form.html',
replace: true,
link: function($scope, element, attrs) {
$scope.widgetSizes = [{name: 'Regular', value: 1}, {name: 'Double', value: 2}];
var reset = function() {
$scope.saveInProgress = false;
$scope.widgetSize = 1;
$scope.queryId = null;
$scope.selectedVis = null;
}
reset();
$scope.toggleView = function(viewName) {
$scope.currentView = ($scope.currentView == viewName) ? '' : viewName;
};
$scope.loadVisualizations = function() {
if (!$scope.queryId) {
return;
}
Query.get({
id: $scope.queryId
}, function(query) {
if (query) {
$scope.query = query;
if(query.visualizations.length) {
$scope.selectedVis = query.visualizations[0];
}
}
});
};
$scope.saveWidget = function() {
$scope.saveInProgress = true;
var widget = {
'visualization_id': $scope.selectedVis.id,
'dashboard_id': $scope.dashboard.id,
'options': {},
'width': $scope.widgetSize
}
$http.post('/api/widgets', widget).success(function(response) {
// update dashboard layout
$scope.dashboard.layout = response['layout'];
if (response['new_row']) {
$scope.dashboard.widgets.push([response['widget']]);
} else {
$scope.dashboard.widgets[$scope.dashboard.widgets.length-1].push(response['widget']);
}
// close the dialog
$('#add_query_dialog').modal('hide');
reset();
})
}
}
}
}])
// From: http://jsfiddle.net/joshdmiller/NDFHg/
directives.directive('editInPlace', function () {
return {
restrict: 'E',
scope: {
value: '=',
ignoreBlanks: '=',
editable: '='
},
template: function(tElement, tAttrs) {
var elType = tAttrs.editor || 'input';
var placeholder = tAttrs.placeholder || 'Click to edit';
return '<span ng-click="editable && edit()" ng-bind="value" ng-class="{editable: editable}"></span>' +
'<span ng-click="editable && edit()" ng-show="editable && !value" ng-class="{editable: editable}">' + placeholder + '</span>' +
'<{elType} ng-model="value" class="form-control" rows="2"></{elType}>'.replace('{elType}', elType);
},
link: function ($scope, element, attrs) {
// Let's get a reference to the input element, as we'll want to reference it.
var inputElement = angular.element(element.children()[2]);
// This directive should have a set class so we can style it.
element.addClass('edit-in-place');
// Initially, we're not editing.
$scope.editing = false;
// ng-click handler to activate edit-in-place
$scope.edit = function () {
if ($scope.ignoreBlanks) {
$scope.oldValue = $scope.value;
}
$scope.editing = true;
// We control display through a class on the directive itself. See the CSS.
element.addClass('active');
// And we must focus the element.
// `angular.element()` provides a chainable array, like jQuery so to access a native DOM function,
// we have to reference the first element in the array.
inputElement[0].focus();
};
$(inputElement).blur(function() {
if ($scope.ignoreBlanks && _.isEmpty($scope.value)) {
$scope.value = $scope.oldValue;
}
$scope.editing = false;
element.removeClass('active');
})
}
};
});
// http://stackoverflow.com/a/17904092/1559840
directives.directive('jsonText', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attr, ngModel) {
function into(input) {
return JSON.parse(input);
}
function out(data) {
return JSON.stringify(data, undefined, 2);
}
ngModel.$parsers.push(into);
ngModel.$formatters.push(out);
}
};
});
directives.directive('rdTimer', ['$timeout', function ($timeout) {
return {
restrict: 'E',
scope: { timestamp: '=' },
template: '{{currentTime}}',
controller: ['$scope' ,function ($scope) {
$scope.currentTime = "00:00:00";
var currentTimeout = null;
var updateTime = function() {
$scope.currentTime = moment(moment() - moment($scope.timestamp)).utc().format("HH:mm:ss")
currentTimeout = $timeout(updateTime, 1000);
}
var cancelTimer = function() {
if (currentTimeout) {
$timeout.cancel(currentTimeout);
currentTimeout = null;
}
}
updateTime();
$scope.$on('$destroy', function () {
cancelTimer();
});
}]
};
}]);
})();

View File

@@ -0,0 +1,208 @@
(function() {
'use strict'
var directives = angular.module('redash.directives');
directives.directive('editDashboardForm', ['Events', '$http', '$location', '$timeout', 'Dashboard',
function(Events, $http, $location, $timeout, Dashboard) {
return {
restrict: 'E',
scope: {
dashboard: '='
},
templateUrl: '/views/edit_dashboard.html',
replace: true,
link: function($scope, element, attrs) {
var gridster = element.find(".gridster ul").gridster({
widget_margins: [5, 5],
widget_base_dimensions: [260, 100],
min_cols: 2,
max_cols: 2,
serialize_params: function($w, wgd) {
return {
col: wgd.col,
row: wgd.row,
id: $w.data('widget-id')
}
}
}).data('gridster');
var gsItemTemplate = '<li data-widget-id="{id}" class="widget panel panel-default gs-w">' +
'<div class="panel-heading">{name}' +
'</div></li>';
$scope.$watch('dashboard.widgets && dashboard.widgets.length', function(widgets_length) {
$timeout(function() {
gridster.remove_all_widgets();
if ($scope.dashboard.widgets && $scope.dashboard.widgets.length) {
var layout = [];
_.each($scope.dashboard.widgets, function(row, rowIndex) {
_.each(row, function(widget, colIndex) {
layout.push({
id: widget.id,
col: colIndex + 1,
row: rowIndex + 1,
ySize: 1,
xSize: widget.width,
name: widget.getName()//visualization.query.name
});
});
});
_.each(layout, function(item) {
var el = gsItemTemplate.replace('{id}', item.id).replace('{name}', item.name);
gridster.add_widget(el, item.xSize, item.ySize, item.col, item.row);
});
}
});
});
$scope.saveDashboard = function() {
$scope.saveInProgress = true;
// TODO: we should use the dashboard service here.
if ($scope.dashboard.id) {
var positions = $(element).find('.gridster ul').data('gridster').serialize();
var layout = [];
_.each(_.sortBy(positions, function(pos) {
return pos.row * 10 + pos.col;
}), function(pos) {
var row = pos.row - 1;
var col = pos.col - 1;
layout[row] = layout[row] || [];
if (col > 0 && layout[row][col - 1] == undefined) {
layout[row][col - 1] = pos.id;
} else {
layout[row][col] = pos.id;
}
});
$scope.dashboard.layout = layout;
layout = JSON.stringify(layout);
$http.post('/api/dashboards/' + $scope.dashboard.id, {
'name': $scope.dashboard.name,
'layout': layout
}).success(function(response) {
$scope.dashboard = new Dashboard(response);
$scope.saveInProgress = false;
$(element).modal('hide');
});
Events.record(currentUser, 'edit', 'dashboard', $scope.dashboard.id);
} else {
$http.post('/api/dashboards', {
'name': $scope.dashboard.name
}).success(function(response) {
$(element).modal('hide');
$scope.dashboard = {
'name': null,
'layout': null
};
$scope.saveInProgress = false;
$location.path('/dashboard/' + response.slug).replace();
});
Events.record(currentUser, 'create', 'dashboard');
}
}
}
}
}
]);
directives.directive('newWidgetForm', ['Query', 'Widget', 'growl',
function(Query, Widget, growl) {
return {
restrict: 'E',
scope: {
dashboard: '='
},
templateUrl: '/views/new_widget_form.html',
replace: true,
link: function($scope, element, attrs) {
$scope.widgetSizes = [{
name: 'Regular',
value: 1
}, {
name: 'Double',
value: 2
}];
$scope.type = 'visualization';
$scope.isVisualization = function () {
return $scope.type == 'visualization';
};
$scope.isTextBox = function () {
return $scope.type == 'textbox';
};
$scope.setType = function (type) {
$scope.type = type;
};
var reset = function() {
$scope.saveInProgress = false;
$scope.widgetSize = 1;
$scope.queryId = null;
$scope.selectedVis = null;
$scope.query = null;
$scope.text = "";
};
reset();
$scope.loadVisualizations = function () {
if (!$scope.queryId) {
return;
}
Query.get({ id: $scope.queryId }, function(query) {
if (query) {
$scope.query = query;
if (query.visualizations.length) {
$scope.selectedVis = query.visualizations[0];
}
}
});
};
$scope.saveWidget = function() {
$scope.saveInProgress = true;
var widget = new Widget({
'visualization_id': $scope.selectedVis && $scope.selectedVis.id,
'dashboard_id': $scope.dashboard.id,
'options': {},
'width': $scope.widgetSize,
'text': $scope.text
});
widget.$save().then(function(response) {
// update dashboard layout
$scope.dashboard.layout = response['layout'];
var newWidget = new Widget(response['widget']);
if (response['new_row']) {
$scope.dashboard.widgets.push([newWidget]);
} else {
$scope.dashboard.widgets[$scope.dashboard.widgets.length - 1].push(newWidget);
}
// close the dialog
$('#add_query_dialog').modal('hide');
reset();
}).catch(function() {
growl.addErrorMessage("Widget can not be added");
}).finally(function() {
$scope.saveInProgress = false;
});
}
}
}
}
])
})();

View File

@@ -0,0 +1,227 @@
(function () {
'use strict';
var directives = angular.module('redash.directives', []);
directives.directive('alertUnsavedChanges', ['$window', function ($window) {
return {
restrict: 'E',
replace: true,
scope: {
'isDirty': '='
},
link: function ($scope) {
var
unloadMessage = "You will lose your changes if you leave",
confirmMessage = unloadMessage + "\n\nAre you sure you want to leave this page?",
// store original handler (if any)
_onbeforeunload = $window.onbeforeunload;
$window.onbeforeunload = function () {
return $scope.isDirty ? unloadMessage : null;
}
$scope.$on('$locationChangeStart', function (event, next, current) {
if (next.split("#")[0] == current.split("#")[0]) {
return;
}
if ($scope.isDirty && !confirm(confirmMessage)) {
event.preventDefault();
}
});
$scope.$on('$destroy', function () {
$window.onbeforeunload = _onbeforeunload;
});
}
}
}]);
directives.directive('rdTab', function () {
return {
restrict: 'E',
scope: {
'tabId': '@',
'name': '@'
},
transclude: true,
template: '<li class="rd-tab" ng-class="{active: tabId==selectedTab}"><a href="#{{tabId}}">{{name}}<span ng-transclude></span></a></li>',
replace: true,
link: function (scope) {
scope.$watch(function () {
return scope.$parent.selectedTab
}, function (tab) {
scope.selectedTab = tab;
});
}
}
});
directives.directive('rdTabs', ['$location', function ($location) {
return {
restrict: 'E',
scope: {
tabsCollection: '=',
selectedTab: '='
},
template: '<ul class="nav nav-tabs"><li ng-class="{active: tab==selectedTab}" ng-repeat="tab in tabsCollection"><a href="#{{tab.key}}">{{tab.name}}</a></li></ul>',
replace: true,
link: function ($scope, element, attrs) {
$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 {
restrict: 'E',
scope: {
value: '=',
ignoreBlanks: '=',
editable: '=',
done: '='
},
template: function (tElement, tAttrs) {
var elType = tAttrs.editor || 'input';
var placeholder = tAttrs.placeholder || 'Click to edit';
return '<span ng-click="editable && edit()" ng-bind="value" ng-class="{editable: editable}"></span>' +
'<span ng-click="editable && edit()" ng-show="editable && !value" ng-class="{editable: editable}">' + placeholder + '</span>' +
'<{elType} ng-model="value" class="rd-form-control"></{elType}>'.replace('{elType}', elType);
},
link: function ($scope, element, attrs) {
// Let's get a reference to the input element, as we'll want to reference it.
var inputElement = angular.element(element.children()[2]);
// This directive should have a set class so we can style it.
element.addClass('edit-in-place');
// Initially, we're not editing.
$scope.editing = false;
// ng-click handler to activate edit-in-place
$scope.edit = function () {
$scope.oldValue = $scope.value;
$scope.editing = true;
// We control display through a class on the directive itself. See the CSS.
element.addClass('active');
// And we must focus the element.
// `angular.element()` provides a chainable array, like jQuery so to access a native DOM function,
// we have to reference the first element in the array.
inputElement[0].focus();
};
function save() {
if ($scope.editing) {
if ($scope.ignoreBlanks && _.isEmpty($scope.value)) {
$scope.value = $scope.oldValue;
}
$scope.editing = false;
element.removeClass('active');
if ($scope.value !== $scope.oldValue) {
$scope.done && $scope.done();
}
}
}
$(inputElement).keydown(function (e) {
// 'return' or 'enter' key pressed
// allow 'shift' to break lines
if (e.which === 13 && !e.shiftKey) {
save();
} else if (e.which === 27) {
$scope.value = $scope.oldValue;
$scope.$apply(function () {
$(inputElement[0]).blur();
});
}
}).blur(function () {
save();
});
}
};
});
// http://stackoverflow.com/a/17904092/1559840
directives.directive('jsonText', function () {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attr, ngModel) {
function into(input) {
return JSON.parse(input);
}
function out(data) {
return JSON.stringify(data, undefined, 2);
}
ngModel.$parsers.push(into);
ngModel.$formatters.push(out);
scope.$watch(attr.ngModel, function (newValue) {
element[0].value = out(newValue);
}, true);
}
};
});
directives.directive('rdTimer', [function () {
return {
restrict: 'E',
scope: { timestamp: '=' },
template: '{{currentTime}}',
controller: ['$scope' , function ($scope) {
$scope.currentTime = "00:00:00";
// We're using setInterval directly instead of $timeout, to avoid using $apply, to
// prevent the digest loop being run every second.
var currentTimer = setInterval(function () {
$scope.currentTime = moment(moment() - moment($scope.timestamp)).utc().format("HH:mm:ss");
$scope.$digest();
}, 1000);
$scope.$on('$destroy', function () {
if (currentTimer) {
clearInterval(currentTimer);
currentTimer = null;
}
});
}]
};
}]);
directives.directive('rdTimeAgo', function () {
return {
restrict: 'E',
scope: {
value: '='
},
template: '<span>' +
'<span ng-show="value" am-time-ago="value"></span>' +
'<span ng-hide="value">-</span>' +
'</span>'
}
});
})();

View File

@@ -0,0 +1,162 @@
(function() {
'use strict'
function queryLink() {
return {
restrict: 'E',
scope: {
'query': '=',
'visualization': '=?'
},
template: '<a ng-href="{{link}}" class="query-link">{{query.name}}</a>',
link: function(scope, element) {
scope.link = '/queries/' + scope.query.id;
if (scope.visualization) {
if (scope.visualization.type === 'TABLE') {
// link to hard-coded table tab instead of the (hidden) visualization tab
scope.link += '#table';
} else {
scope.link += '#' + scope.visualization.id;
}
}
// element.find('a').attr('href', link);
}
}
}
function querySourceLink() {
return {
restrict: 'E',
template: '<span ng-show="query.id && canViewSource">\
<a ng-show="!sourceMode"\
ng-href="{{query.id}}/source#{{selectedTab}}">Show Source\
</a>\
<a ng-show="sourceMode"\
ng-href="/queries/{{query.id}}#{{selectedTab}}">Hide Source\
</a>\
</span>'
}
}
function queryResultCSVLink() {
return {
restrict: 'A',
link: function (scope, element) {
scope.$watch('queryResult && queryResult.getData()', function(data) {
if (!data) {
return;
}
if (scope.queryResult.getId() == null) {
element.attr('href', '');
} else {
element.attr('href', '/api/queries/' + scope.query.id + '/results/' + scope.queryResult.getId() + '.csv');
element.attr('download', scope.query.name.replace(" ", "_") + moment(scope.queryResult.getUpdatedAt()).format("_YYYY_MM_DD") + ".csv");
}
});
}
}
}
function queryEditor() {
return {
restrict: 'E',
scope: {
'query': '=',
'lock': '='
},
template: '<textarea\
ui-codemirror="editorOptions"\
ng-model="query.query">',
link: function($scope) {
$scope.editorOptions = {
mode: 'text/x-sql',
lineWrapping: true,
lineNumbers: true,
readOnly: false,
matchBrackets: true,
autoCloseBrackets: true
};
$scope.$watch('lock', function(locked) {
$scope.editorOptions.readOnly = locked ? 'nocursor' : false;
});
}
}
}
function queryFormatter($http) {
return {
restrict: 'E',
// don't create new scope to avoid ui-codemirror bug
// seehttps://github.com/angular-ui/ui-codemirror/pull/37
scope: false,
template: '<button type="button" class="btn btn-default btn-xs"\
ng-click="formatQuery()">\
<span class="glyphicon glyphicon-indent-left"></span>\
Format SQL\
</button>',
link: function($scope) {
$scope.formatQuery = function formatQuery() {
$scope.queryExecuting = true;
$http.post('/api/queries/format', {
'query': $scope.query.query
}).success(function (response) {
$scope.query.query = response;
}).finally(function () {
$scope.queryExecuting = false;
});
};
}
}
}
function queryRefreshSelect() {
return {
restrict: 'E',
template: '<select\
ng-disabled="!isQueryOwner"\
ng-model="query.ttl"\
ng-change="saveQuery()"\
ng-options="c.value as c.name for c in refreshOptions">\
</select>',
link: function($scope) {
$scope.refreshOptions = [
{
value: -1,
name: 'No Refresh'
},
{
value: 60,
name: 'Every minute'
},
]
_.each(_.range(1, 13), function (i) {
$scope.refreshOptions.push({
value: i * 3600,
name: 'Every ' + i + 'h'
});
})
$scope.refreshOptions.push({
value: 24 * 3600,
name: 'Every 24h'
});
$scope.refreshOptions.push({
value: 7 * 24 * 3600,
name: 'Once a week'
});
}
}
}
angular.module('redash.directives')
.directive('queryLink', queryLink)
.directive('querySourceLink', querySourceLink)
.directive('queryResultLink', queryResultCSVLink)
.directive('queryEditor', queryEditor)
.directive('queryRefreshSelect', queryRefreshSelect)
.directive('queryFormatter', ['$http', queryFormatter]);
})();

View File

@@ -1,50 +1,75 @@
var durationHumanize = function (duration) {
var humanized = "";
if (duration == undefined) {
humanized = "-";
} else if (duration < 60) {
humanized = Math.round(duration) + "s";
} else if (duration > 3600*24) {
var days = Math.round(parseFloat(duration) / 60.0 / 60.0 / 24.0);
humanized = days + "days";
} else if (duration >= 3600) {
var hours = Math.round(parseFloat(duration) / 60.0 / 60.0);
humanized = hours + "h";
} else {
var minutes = Math.round(parseFloat(duration) / 60.0);
humanized = minutes + "m";
}
return humanized;
}
var humanized = "";
if (duration == undefined) {
humanized = "-";
} else if (duration < 60) {
humanized = Math.round(duration) + "s";
} else if (duration > 3600 * 24) {
var days = Math.round(parseFloat(duration) / 60.0 / 60.0 / 24.0);
humanized = days + "days";
} else if (duration >= 3600) {
var hours = Math.round(parseFloat(duration) / 60.0 / 60.0);
humanized = hours + "h";
} else {
var minutes = Math.round(parseFloat(duration) / 60.0);
humanized = minutes + "m";
}
return humanized;
};
var urlPattern = /(^|[\s\n]|<br\/?>)((?:https?|ftp):\/\/[\-A-Z0-9+\u0026\u2019@#\/%?=()~_|!:,.;]*[\-A-Z0-9+\u0026@#\/%=~()_|])/gi;
angular.module('redash.filters', []).
filter('durationHumanize', function () {
return durationHumanize;
})
filter('durationHumanize', function () {
return durationHumanize;
})
.filter('refreshRateHumanize', function () {
return function (ttl) {
if (ttl==-1) {
return "Never";
} else {
return "Every " + durationHumanize(ttl);
}
}
})
.filter('refreshRateHumanize', function () {
return function (ttl) {
if (ttl == -1) {
return "Never";
} else {
return "Every " + durationHumanize(ttl);
}
}
})
.filter('toHuman', function() {
return function(text) {
return text.replace(/_/g, ' ').replace(/(?:^|\s)\S/g, function (a) {
return a.toUpperCase();
});
}
})
.filter('toHuman', function () {
return function (text) {
return text.replace(/_/g, ' ').replace(/(?:^|\s)\S/g, function (a) {
return a.toUpperCase();
});
}
})
.filter('colWidth', function () {
return function (widgetWidth) {
if (widgetWidth == 1) {
return 6;
}
return 12;
}
});
.filter('colWidth', function () {
return function (widgetWidth) {
if (widgetWidth == 1) {
return 6;
}
return 12;
}
})
.filter('capitalize', function () {
return function (text) {
if (text) {
return _.str.capitalize(text);
} else {
return null;
}
}
})
.filter('linkify', function () {
return function (text) {
return text.replace(urlPattern, "$1<a href='$2' target='_blank'>$2</a>");
};
})
.filter('markdown', ['$sce', function ($sce) {
return function (text) {
return $sce.trustAsHtml(marked(text));
}
}]);

View File

@@ -1,181 +0,0 @@
(function(){
'use strict';
var defaultOptions = {
title: {
"text": null
},
tooltip: {
valueDecimals: 2,
formatter: function () {
if (moment.isMoment(this.x)) {
var s = '<b>' + moment(this.x).format("DD/MM/YY HH:mm") + '</b>',
pointsCount = this.points.length;
$.each(this.points, function (i, point) {
s += '<br/><span style="color:'+point.series.color+'">' + point.series.name + '</span>: ' +
Highcharts.numberFormat(point.y);
if (pointsCount > 1 && point.percentage) {
s += " (" + Highcharts.numberFormat(point.percentage) + "%)";
}
});
} else {
var s = "<b>" + this.points[0].key + "</b>";
$.each(this.points, function (i, point) {
s+= '<br/><span style="color:'+point.series.color+'">' + point.series.name + '</span>: ' +
Highcharts.numberFormat(point.y);
});
}
return s;
},
shared: true
},
xAxis: {
type: 'datetime'
},
yAxis: {
title: {
text: null
}
},
exporting: {
chartOptions: {
title: {
text: ''
}
},
buttons: {
contextButton: {
menuItems: [
{
text: 'Toggle % Stacking',
onclick: function () {
var newStacking = "normal";
if (this.series[0].options.stacking == "normal") {
newStacking = "percent";
}
_.each(this.series, function (series) {
series.update({stacking: newStacking}, true);
});
}
}
]
}
}
},
credits: {
enabled: false
},
plotOptions: {
"column": {
"stacking": "normal",
"pointPadding": 0,
"borderWidth": 1,
"groupPadding": 0,
"shadow": false
}
},
series: []
};
angular.module('highchart', [])
.directive('chart', ['$timeout', function ($timeout) {
return {
restrict: 'E',
template: '<div></div>',
scope: {
options: "=options",
series: "=series"
},
transclude: true,
replace: true,
link: function (scope, element, attrs) {
var chartsDefaults = {
chart: {
renderTo: element[0],
type: attrs.type || null,
height: attrs.height || null,
width: attrs.width || null
}
};
var chartOptions = $.extend(true, {}, defaultOptions, chartsDefaults);
// Update when options change
scope.$watch('options', function(newOptions) {
initChart(newOptions);
}, true);
//Update when charts data changes
scope.$watch(function () {
return (scope.series && scope.series.length) || 0;
}, function (length) {
if (!length || length == 0) {
scope.chart.showLoading();
} else {
drawChart();
};
}, true);
function initChart(options) {
if (scope.chart) {
scope.chart.destroy();
}
$.extend(true, chartOptions, options);
scope.chart = new Highcharts.Chart(chartOptions);
drawChart();
}
function drawChart() {
while(scope.chart.series.length > 0) {
scope.chart.series[0].remove(true);
}
// todo series.type
if (_.some(scope.series[0].data, function(p) { return angular.isString(p.x) })) {
scope.chart.xAxis[0].update({type: 'category'});
// We need to make sure that for each category, each series has a value.
var categories = _.union.apply(this, _.map(scope.series, function(s) { return _.pluck(s.data,'x')}));
_.each(scope.series, function(s) {
// TODO: move this logic to Query#getChartData
var yValues = _.groupBy(s.data, 'x');
var newData = _.sortBy(_.map(categories, function(category) {
return {
name: category,
y: yValues[category] && yValues[category][0].y
}
}), 'name');
s.data = newData;
});
} else {
scope.chart.xAxis[0].update({type: 'datetime'});
}
scope.chart.counters.color = 0;
_.each(scope.series, function(s) {
// here we override the series with the visualization config
var _s = $.extend(true, {}, s, chartOptions['series']);
scope.chart.addSeries(_s);
})
scope.chart.redraw();
scope.chart.hideLoading();
}
}
};
}]);
})();

View File

@@ -0,0 +1,341 @@
(function () {
'use strict';
Highcharts.setOptions({
colors: ["#4572A7", "#AA4643", "#89A54E", "#80699B", "#3D96AE",
"#DB843D", "#92A8CD", "#A47D7C", "#B5CA92"]
});
var defaultOptions = {
title: {
"text": null
},
xAxis: {
type: 'datetime'
},
yAxis: [
{
title: {
text: null
},
// showEmpty: true // by default
},
{
title: {
text: null
},
opposite: true,
showEmpty: false
}
],
tooltip: {
valueDecimals: 2,
formatter: function () {
if (!this.points) {
this.points = [this.point];
}
;
if (moment.isMoment(this.x)) {
var s = '<b>' + moment(this.x).format("DD/MM/YY HH:mm") + '</b>',
pointsCount = this.points.length;
$.each(this.points, function (i, point) {
s += '<br/><span style="color:' + point.series.color + '">' + point.series.name + '</span>: ' +
Highcharts.numberFormat(point.y);
if (pointsCount > 1 && point.percentage) {
s += " (" + Highcharts.numberFormat(point.percentage) + "%)";
}
});
} else {
var points = this.points;
var name = points[0].key || points[0].name;
var s = "<b>" + name + "</b>";
$.each(points, function (i, point) {
if (points.length > 1) {
s += '<br/><span style="color:' + point.series.color + '">' + point.series.name + '</span>: ' + Highcharts.numberFormat(point.y);
} else {
s += ": " + Highcharts.numberFormat(point.y);
if (point.percentage < 100) {
s += ' (' + Highcharts.numberFormat(point.percentage) + '%)';
}
}
});
}
return s;
},
shared: true
},
exporting: {
chartOptions: {
title: {
text: ''
}
},
buttons: {
contextButton: {
menuItems: [
{
text: 'Toggle % Stacking',
onclick: function () {
var newStacking = "normal";
if (this.series[0].options.stacking == "normal") {
newStacking = "percent";
}
_.each(this.series, function (series) {
series.update({stacking: newStacking}, true);
});
}
},
{
text: 'Select All',
onclick: function () {
_.each(this.series, function (s) {
s.setVisible(true, false);
});
this.redraw();
}
},
{
text: 'Unselect All',
onclick: function () {
_.each(this.series, function (s) {
s.setVisible(false, false);
});
this.redraw();
}
},
{
text: 'Show Total',
onclick: function () {
var data = {};
_.each(this.series, function (s) {
s.setVisible(false, false);
_.each(s.data, function (p) {
data[p.x] = data[p.x] || {'x': p.x, 'y': 0};
data[p.x].y = data[p.x].y + p.y;
});
});
this.addSeries({
data: _.values(data),
type: 'line',
name: 'Total'
}, false)
this.redraw();
}
}
]
}
}
},
credits: {
enabled: false
},
plotOptions: {
area: {
marker: {
enabled: false,
symbol: 'circle',
radius: 2,
states: {
hover: {
enabled: true
}
}
}
},
column: {
stacking: "normal",
pointPadding: 0,
borderWidth: 1,
groupPadding: 0,
shadow: false
},
line: {
marker: {
radius: 1
},
lineWidth: 2,
states: {
hover: {
lineWidth: 2,
marker: {
radius: 3
}
}
}
},
pie: {
allowPointSelect: true,
cursor: 'pointer',
dataLabels: {
enabled: true,
color: '#000000',
connectorColor: '#000000',
format: '<b>{point.name}</b>: {point.y} ({point.percentage:.1f} %)'
}
},
scatter: {
marker: {
radius: 5,
states: {
hover: {
enabled: true,
lineColor: 'rgb(100,100,100)'
}
}
},
tooltip: {
headerFormat: '<b>{series.name}</b><br>',
pointFormat: '{point.x}, {point.y}'
}
}
},
series: []
};
angular.module('highchart', [])
.directive('chart', ['$timeout', function ($timeout) {
return {
restrict: 'E',
template: '<div></div>',
scope: {
options: "=options",
series: "=series"
},
transclude: true,
replace: true,
link: function (scope, element, attrs) {
var chartsDefaults = {
chart: {
renderTo: element[0],
type: attrs.type || null,
height: attrs.height || null,
width: attrs.width || null
}
};
var chartOptions = $.extend(true, {}, defaultOptions, chartsDefaults);
// $timeout makes sure that this function invoked after the DOM ready. When draw/init
// invoked after the DOM is ready, we see first an empty HighCharts objects and later
// they get filled up. Which gives the feeling that the charts loading faster (otherwise
// we stare at an empty screen until the HighCharts object is ready).
$timeout(function () {
// Update when options change
scope.$watch('options', function (newOptions) {
initChart(newOptions);
}, true);
//Update when charts data changes
scope.$watchCollection('series', function (series) {
if (!series || series.length == 0) {
scope.chart.showLoading();
} else {
drawChart();
}
;
});
});
function initChart(options) {
if (scope.chart) {
scope.chart.destroy();
}
;
$.extend(true, chartOptions, options);
scope.chart = new Highcharts.Chart(chartOptions);
drawChart();
}
function drawChart() {
while (scope.chart.series.length > 0) {
scope.chart.series[0].remove(false);
};
if (!('xAxis' in chartOptions && 'type' in chartOptions['xAxis'])) {
if (scope.series.length > 0 && _.some(scope.series[0].data, function (p) {
return (angular.isString(p.x) || angular.isDefined(p.name));
})) {
chartOptions['xAxis'] = chartOptions['xAxis'] || {};
chartOptions['xAxis']['type'] = 'category';
} else {
chartOptions['xAxis'] = chartOptions['xAxis'] || {};
chartOptions['xAxis']['type'] = 'datetime';
}
}
if (chartOptions['xAxis']['type'] == 'category' || chartOptions['series']['type']=='pie') {
if (!angular.isDefined(scope.series[0].data[0].name)) {
// We need to make sure that for each category, each series has a value.
var categories = _.union.apply(this, _.map(scope.series, function (s) {
return _.pluck(s.data, 'x')
}));
_.each(scope.series, function (s) {
// TODO: move this logic to Query#getChartData
var yValues = _.groupBy(s.data, 'x');
var newData = _.map(categories, function (category) {
return {
name: category,
y: (yValues[category] && yValues[category][0].y) || 0
}
});
if (categories.length == 1) {
newData = _.sortBy(newData, 'y').reverse();
}
;
s.data = newData;
});
}
}
scope.chart.counters.color = 0;
_.each(scope.series, function (s) {
// here we override the series with the visualization config
s = _.extend(s, chartOptions['series']);
if (s.type == 'area') {
_.each(s.data, function (p) {
// This is an insane hack: somewhere deep in HighChart's code,
// when you stack areas, it tries to convert the string representation
// of point's x into a number. With the default implementation of toString
// it fails....
if (moment.isMoment(p.x)) {
p.x.toString = function () {
return String(this.toDate().getTime());
};
}
});
}
;
scope.chart.addSeries(s, false);
});
scope.chart.redraw();
scope.chart.hideLoading();
}
}
};
}]);
})();

View File

@@ -217,7 +217,7 @@
element.html('<div editable-cell="" row="dataRow" column="column" type="column.type"></div>');
compile(element.contents())(scope);
} else {
element.text(scope.formatedValue);
element.html(scope.formatedValue);
}
}

View File

@@ -1,203 +0,0 @@
var renderers = angular.module('redash.renderers', []);
renderers.directive('visualizationRenderer', function() {
return {
restrict: 'E',
scope: {
visualization: '=',
queryResult: '='
},
template: '<div ng-switch on="visualization.type">' +
'<chart-renderer ng-switch-when="CHART" options="visualization.options" query-result="queryResult"></chart-renderer>' +
'<grid-renderer ng-switch-when="GRID" options="visualization.options" query-result="queryResult"></grid-renderer>' +
'<cohort-renderer ng-switch-when="COHORT" options="visualization.options" query-result="queryResult"></cohort-renderer>' +
'</div>',
replace: false
}
});
renderers.directive('chartRenderer', function () {
return {
restrict: 'E',
scope: {
queryResult: '=',
options: '=?'
},
template: "<chart options='chartOptions' series='chartSeries' class='graph'></chart>",
replace: false,
controller: ['$scope', function ($scope) {
$scope.chartSeries = [];
$scope.chartOptions = {};
$scope.$watch('options', function(chartOptions) {
if (chartOptions) {
$scope.chartOptions = chartOptions;
}
});
$scope.$watch('queryResult && queryResult.getData()', function (data) {
if (!data || $scope.queryResult.getData() == null) {
$scope.chartSeries.splice(0, $scope.chartSeries.length);
} else {
$scope.chartSeries.splice(0, $scope.chartSeries.length);
_.each($scope.queryResult.getChartData(), function (s) {
$scope.chartSeries.push(_.extend(s, {'stacking': 'normal'}));
});
}
});
}]
}
})
renderers.directive('gridRenderer', function () {
return {
restrict: 'E',
scope: {
queryResult: '=',
itemsPerPage: '='
},
templateUrl: "/views/grid_renderer.html",
replace: false,
controller: ['$scope', function ($scope) {
$scope.gridColumns = [];
$scope.gridData = [];
$scope.gridConfig = {
isPaginationEnabled: true,
itemsByPage: $scope.itemsPerPage || 15,
maxSize: 8
};
$scope.$watch('queryResult && queryResult.getData()', function (data) {
if (!data) {
return;
}
if ($scope.queryResult.getData() == null) {
$scope.gridColumns = [];
$scope.gridData = [];
$scope.filters = [];
} else {
$scope.filters = $scope.queryResult.getFilters();
var gridData = _.map($scope.queryResult.getData(), function (row) {
var newRow = {};
_.each(row, function (val, key) {
newRow[$scope.queryResult.getColumnCleanName(key)] = val;
})
return newRow;
});
$scope.gridColumns = _.map($scope.queryResult.getColumnCleanNames(), function (col, i) {
var columnDefinition = {
'label': $scope.queryResult.getColumnFriendlyNames()[i],
'map': col
};
if (gridData.length > 0) {
var exampleData = gridData[0][col];
if (angular.isNumber(exampleData)) {
columnDefinition['formatFunction'] = 'number';
columnDefinition['formatParameter'] = 2;
} else if (moment.isMoment(exampleData)) {
columnDefinition['formatFunction'] = function(value) {
return value.format("DD/MM/YY HH:mm");
}
}
}
return columnDefinition;
});
$scope.gridData = _.clone(gridData);
$scope.$watch('filters', function (filters) {
$scope.gridData = _.filter(gridData, function (row) {
return _.reduce(filters, function (memo, filter) {
if (filter.current == 'All') {
return memo && true;
}
return (memo && row[$scope.queryResult.getColumnCleanName(filter.name)] == filter.current);
}, true);
});
}, true);
}
});
}]
}
})
renderers.directive('pivotTableRenderer', function () {
return {
restrict: 'E',
scope: {
queryResult: '='
},
template: "",
replace: false,
link: function($scope, element, attrs) {
$scope.$watch('queryResult && queryResult.getData()', function (data) {
if (!data) {
return;
}
if ($scope.queryResult.getData() == null) {
} else {
$(element).pivotUI($scope.queryResult.getData(), {
renderers: $.pivotUtilities.renderers
}, true);
}
});
}
}
})
renderers.directive('cohortRenderer', function() {
return {
restrict: 'E',
scope: {
queryResult: '='
},
template: "",
replace: false,
link: function($scope, element, attrs) {
$scope.$watch('queryResult && queryResult.getData()', function (data) {
if (!data) {
return;
}
if ($scope.queryResult.getData() == null) {
} else {
var sortedData = _.sortBy($scope.queryResult.getData(), "date");
var grouped = _.groupBy(sortedData, "date");
var data = _.map(grouped, function(values, date) {
var row = [values[0].total];
_.each(values, function(value) { row.push(value.value); });
return row;
});
var initialDate = moment(sortedData[0].date).toDate(),
container = angular.element(element)[0];
Cornelius.draw({
initialDate: initialDate,
container: container,
cohort: data,
title: null,
timeInterval: 'daily',
labels: {
time: 'Activation Day',
people: 'Users'
},
formatHeaderLabel: function (i) {
return "Day " + (i - 1);
}
});
}
});
}
}
})

View File

@@ -1,310 +0,0 @@
(function () {
var QueryResult = function($resource, $timeout) {
var QueryResultResource = $resource('/api/query_results/:id', {id: '@id'}, {'post': {'method': 'POST'}});
var Job = $resource('/api/jobs/:id', {id: '@id'});
var updateFunction = function (props) {
angular.extend(this, props);
if ('query_result' in props) {
this.status = "done";
_.each(this.query_result.data.rows, function (row) {
_.each(row, function (v, k) {
if (_.isString(v) && v.match(/^\d{4}-\d{2}-\d{2}/)) {
row[k] = moment(v);
}
});
});
} else if (this.job.status == 3) {
this.status = "processing";
} else {
this.status = undefined;
}
}
function QueryResult(props) {
this.job = {};
this.query_result = {};
this.status = "waiting";
this.updatedAt = moment();
if (props) {
updateFunction.apply(this, [props]);
}
}
var statuses = {
1: "waiting",
2: "processing",
3: "done",
4: "failed"
}
QueryResult.prototype.update = updateFunction;
QueryResult.prototype.getId = function() {
var id = null;
if ('query_result' in this) {
id = this.query_result.id;
}
return id;
}
QueryResult.prototype.cancelExecution = function() {
Job.delete({id: this.job.id});
}
QueryResult.prototype.getStatus = function() {
return this.status || statuses[this.job.status];
}
QueryResult.prototype.getError = function() {
// TODO: move this logic to the server...
if (this.job.error == "None") {
return undefined;
}
return this.job.error;
}
QueryResult.prototype.getUpdatedAt = function() {
return this.query_result.retrieved_at || this.job.updated_at*1000.0 || this.updatedAt;
}
QueryResult.prototype.getRuntime = function() {
return this.query_result.runtime;
}
QueryResult.prototype.getData = function() {
if (!this.query_result.data) {
return null;
}
var data = this.query_result.data.rows;
return data;
}
QueryResult.prototype.getChartData = function () {
var series = {};
_.each(this.getData(), function (row) {
var point = {};
var seriesName = undefined;
var xValue = 0;
var yValues = {};
_.each(row, function (value, definition) {
var type = definition.split("::")[1];
var name = definition.split("::")[0];
if (type == 'x') {
xValue = value;
point[type] = value;
}
if (type == 'y') {
yValues[name] = value;
point[type] = value;
}
if (type == 'series') {
seriesName = String(value);
}
});
var addPointToSeries = function(seriesName, point) {
if (series[seriesName] == undefined) {
series[seriesName] = {
name: seriesName,
type: 'column',
data: []
}
}
series[seriesName]['data'].push(point);
}
if (seriesName === undefined) {
_.each(yValues, function(yValue, seriesName) {
addPointToSeries(seriesName, {'x': xValue, 'y': yValue});
});
} else {
addPointToSeries(seriesName, point);
}
});
_.each(series, function(series) {
series.data = _.sortBy(series.data, 'x');
});
return _.values(series);
};
QueryResult.prototype.getColumns = function () {
if (this.columns == undefined) {
this.columns = _.map(this.query_result.data.columns, function(v) {
return v.name;
})
}
return this.columns;
}
QueryResult.prototype.getColumnCleanName = function (column) {
var parts = column.split('::');
var name = parts[1];
if (parts[0] != '') {
// TODO: it's probably time to generalize this.
// see also getColumnFriendlyName
name = parts[0].replace(/%/g, '__pct').replace(/ /g, '_').replace(/\?/g,'');
}
return name;
}
QueryResult.prototype.getColumnFriendlyName = function (column) {
return this.getColumnCleanName(column).replace('__pct', '%').replace(/_/g, ' ').replace(/(?:^|\s)\S/g, function (a) {
return a.toUpperCase();
});
}
QueryResult.prototype.getColumnCleanNames = function () {
return _.map(this.getColumns(), function (col) {
return this.getColumnCleanName(col);
}, this);
}
QueryResult.prototype.getColumnFriendlyNames = function () {
return _.map(this.getColumns(), function (col) {
return this.getColumnFriendlyName(col);
}, this);
}
QueryResult.prototype.getFilters = function () {
var filterNames = [];
_.each(this.getColumns(), function (col) {
if (col.split('::')[1] == 'filter') {
filterNames.push(col);
}
});
var filterValues = [];
_.each(this.getData(), function (row) {
_.each(filterNames, function (filter, i) {
if (filterValues[i] == undefined) {
filterValues[i] = [];
}
filterValues[i].push(row[filter]);
})
});
var filters = _.map(filterNames, function (filter, i) {
var f = {
name: filter,
friendlyName: this.getColumnFriendlyName(filter),
values: _.uniq(filterValues[i])
};
f.current = f.values[0];
return f;
}, this);
return filters;
};
var refreshStatus = function(queryResult, query, ttl) {
Job.get({'id': queryResult.job.id}, function(response) {
queryResult.update(response);
if (queryResult.getStatus() == "processing" && queryResult.job.query_result_id && queryResult.job.query_result_id != "None") {
QueryResultResource.get({'id': queryResult.job.query_result_id}, function(response) {
queryResult.update(response);
});
} else if (queryResult.getStatus() != "failed") {
$timeout(function () {
refreshStatus(queryResult, query, ttl);
}, 3000);
}
})
}
QueryResult.getById = function (id) {
var queryResult = new QueryResult();
QueryResultResource.get({'id': id}, function (response) {
queryResult.update(response);
});
return queryResult;
}
QueryResult.get = function (query, ttl) {
var queryResult = new QueryResult();
QueryResultResource.post({'query': query, 'ttl': ttl}, function (response) {
queryResult.update(response);
if ('job' in response) {
refreshStatus(queryResult, query, ttl);
}
});
return queryResult;
}
return QueryResult;
};
var Query = function ($resource, QueryResult) {
var Query = $resource('/api/queries/:id', {id: '@id'});
Query.prototype.getQueryResult = function(ttl) {
if (ttl == undefined) {
ttl = this.ttl;
}
var queryResult = null;
if (this.latest_query_data && ttl != 0) {
queryResult = new QueryResult({'query_result': this.latest_query_data});
} else if (this.latest_query_data_id && ttl != 0) {
queryResult = QueryResult.getById(this.latest_query_data_id);
} else {
queryResult = QueryResult.get(this.query, ttl);
}
return queryResult;
};
Query.prototype.getHash = function() {
return [this.name, this.description, this.query].join('!#');
};
return Query;
};
var Visualization = function($resource) {
var Visualization = $resource('/api/visualizations/:id', {id: '@id'});
Visualization.prototype = {
TYPES: {
'CHART': 'CHART',
'COHORT': 'COHORT',
'GRID': 'GRID'
},
SERIES_TYPES: {
'LINE': 'line',
'BAR': 'bar',
'AREA': 'area'
}
};
return Visualization;
};
angular.module('redash.services', [])
.factory('QueryResult', ['$resource', '$timeout', QueryResult])
.factory('Query', ['$resource', 'QueryResult', Query])
.factory('Visualization', ['$resource', Visualization])
})();

View File

@@ -2,7 +2,7 @@
var Dashboard = function($resource) {
var resource = $resource('/api/dashboards/:slug', {slug: '@slug'});
resource.prototype.canEdit = function() {
return currentUser.is_admin || currentUser.canEdit(this);
return currentUser.hasPermission('admin') || currentUser.canEdit(this);
}
return resource;
}

View File

@@ -1,5 +1,5 @@
(function () {
var notifications = function () {
var notifications = function (Events) {
var notificationService = {};
var lastNotification = null;
@@ -40,6 +40,7 @@
notification.onclick = function () {
window.focus();
this.cancel();
Events.record(currentUser, 'click', 'notification');
};
notification.show()
@@ -49,5 +50,5 @@
}
angular.module('redash.services')
.factory('notifications', notifications);
.factory('notifications', ['Events', notifications]);
})();

View File

@@ -0,0 +1,454 @@
(function () {
var QueryResult = function ($resource, $timeout, $q) {
var QueryResultResource = $resource('/api/query_results/:id', {id: '@id'}, {'post': {'method': 'POST'}});
var Job = $resource('/api/jobs/:id', {id: '@id'});
var updateFunction = function (props) {
angular.extend(this, props);
if ('query_result' in props) {
this.status = "done";
this.filters = undefined;
this.filterFreeze = undefined;
var columnTypes = {};
_.each(this.query_result.data.rows, function (row) {
_.each(row, function (v, k) {
if (angular.isNumber(v)) {
columnTypes[k] = 'float';
} else if (_.isString(v) && v.match(/^\d{4}-\d{2}-\d{2}T/)) {
row[k] = moment(v);
columnTypes[k] = 'datetime';
} else if (_.isString(v) && v.match(/^\d{4}-\d{2}-\d{2}/)) {
row[k] = moment(v);
columnTypes[k] = 'date';
}
}, this);
}, this);
_.each(this.query_result.data.columns, function(column) {
if (columnTypes[column.name]) {
column.type = columnTypes[column.name];
}
});
this.deferred.resolve(this);
} else if (this.job.status == 3) {
this.status = "processing";
} else {
this.status = undefined;
}
}
function QueryResult(props) {
this.deferred = $q.defer();
this.job = {};
this.query_result = {};
this.status = "waiting";
this.filters = undefined;
this.filterFreeze = undefined;
this.updatedAt = moment();
if (props) {
updateFunction.apply(this, [props]);
}
}
var statuses = {
1: "waiting",
2: "processing",
3: "done",
4: "failed"
}
QueryResult.prototype.update = updateFunction;
QueryResult.prototype.getId = function () {
var id = null;
if ('query_result' in this) {
id = this.query_result.id;
}
return id;
}
QueryResult.prototype.cancelExecution = function () {
Job.delete({id: this.job.id});
}
QueryResult.prototype.getStatus = function () {
return this.status || statuses[this.job.status];
}
QueryResult.prototype.getError = function () {
// TODO: move this logic to the server...
if (this.job.error == "None") {
return undefined;
}
return this.job.error;
}
QueryResult.prototype.getUpdatedAt = function () {
return this.query_result.retrieved_at || this.job.updated_at * 1000.0 || this.updatedAt;
}
QueryResult.prototype.getRuntime = function () {
return this.query_result.runtime;
}
QueryResult.prototype.getRawData = function () {
if (!this.query_result.data) {
return null;
}
var data = this.query_result.data.rows;
return data;
}
QueryResult.prototype.getData = function () {
if (!this.query_result.data) {
return null;
}
var filterValues = function (filters) {
if (!filters) {
return null;
}
return _.reduce(filters, function (str, filter) {
return str + filter.current;
}, "")
}
var filters = this.getFilters();
var filterFreeze = filterValues(filters);
if (this.filterFreeze != filterFreeze) {
this.filterFreeze = filterFreeze;
if (filters) {
this.filteredData = _.filter(this.query_result.data.rows, function (row) {
return _.reduce(filters, function (memo, filter) {
if (!_.isArray(filter.current)) {
filter.current = [filter.current];
};
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
}));
}, true);
});
} else {
this.filteredData = this.query_result.data.rows;
}
}
return this.filteredData;
}
QueryResult.prototype.getChartData = function (mapping) {
var series = {};
_.each(this.getData(), function (row) {
var point = {};
var seriesName = undefined;
var xValue = 0;
var yValues = {};
_.each(row, function (value, definition) {
var name = definition.split("::")[0];
var type = definition.split("::")[1];
if (mapping) {
type = mapping[definition];
}
if (type == 'unused') {
return;
}
if (type == 'x') {
xValue = value;
point[type] = value;
}
if (type == 'y') {
if (value == null) {
value = 0;
}
yValues[name] = value;
point[type] = value;
}
if (type == 'series') {
seriesName = String(value);
}
if (type == 'multi-filter') {
seriesName = String(value);
}
});
var addPointToSeries = function (seriesName, point) {
if (series[seriesName] == undefined) {
series[seriesName] = {
name: seriesName,
type: 'column',
data: []
}
}
series[seriesName]['data'].push(point);
}
if (seriesName === undefined) {
_.each(yValues, function (yValue, seriesName) {
addPointToSeries(seriesName, {'x': xValue, 'y': yValue});
});
} else {
addPointToSeries(seriesName, point);
}
});
_.each(series, function (series) {
series.data = _.sortBy(series.data, 'x');
});
return _.values(series);
};
QueryResult.prototype.getColumns = function () {
if (this.columns == undefined && this.query_result.data) {
this.columns = this.query_result.data.columns;
}
return this.columns;
}
QueryResult.prototype.getColumnNames = function () {
if (this.columnNames == undefined && this.query_result.data) {
this.columnNames = _.map(this.query_result.data.columns, function (v) {
return v.name;
});
}
return this.columnNames;
}
QueryResult.prototype.getColumnNameWithoutType = function (column) {
var parts = column.split('::');
if (parts[0] == "" && parts.length == 2) {
return parts[1];
}
return parts[0];
};
var charConversionMap = {
'__pct': /%/g,
'_': / /g,
'__qm': /\?/g,
'__brkt': /[\(\)\[\]]/g,
'__dash': /-/g,
'__amp': /&/g,
'__sl': /\//g,
'__fsl': /\\/g,
};
QueryResult.prototype.getColumnCleanName = function (column) {
var name = this.getColumnNameWithoutType(column);
if (name != '') {
_.each(charConversionMap, function(regex, replacement) {
name = name.replace(regex, replacement);
});
}
return name;
}
QueryResult.prototype.getColumnFriendlyName = function (column) {
return this.getColumnNameWithoutType(column).replace(/(?:^|\s)\S/g, function (a) {
return a.toUpperCase();
});
}
QueryResult.prototype.getColumnCleanNames = function () {
return _.map(this.getColumnNames(), function (col) {
return this.getColumnCleanName(col);
}, this);
}
QueryResult.prototype.getColumnFriendlyNames = function () {
return _.map(this.getColumnNames(), function (col) {
return this.getColumnFriendlyName(col);
}, this);
}
QueryResult.prototype.getFilters = function () {
if (!this.filters) {
this.prepareFilters();
}
return this.filters;
};
QueryResult.prototype.prepareFilters = function () {
var filters = [];
var filterTypes = ['filter', 'multi-filter'];
_.each(this.getColumnNames(), function (col) {
var type = col.split('::')[1]
if (_.contains(filterTypes, type)) {
// filter found
var filter = {
name: col,
friendlyName: this.getColumnFriendlyName(col),
values: [],
multiple: (type=='multi-filter')
}
filters.push(filter);
}
}, this);
_.each(this.getRawData(), function (row) {
_.each(filters, function (filter) {
filter.values.push(row[filter.name]);
if (filter.values.length == 1) {
filter.current = row[filter.name];
}
})
});
_.each(filters, function(filter) {
filter.values = _.uniq(filter.values);
});
this.filters = filters;
}
var refreshStatus = function (queryResult, query, ttl) {
Job.get({'id': queryResult.job.id}, function (response) {
queryResult.update(response);
if (queryResult.getStatus() == "processing" && queryResult.job.query_result_id && queryResult.job.query_result_id != "None") {
QueryResultResource.get({'id': queryResult.job.query_result_id}, function (response) {
queryResult.update(response);
});
} else if (queryResult.getStatus() != "failed") {
$timeout(function () {
refreshStatus(queryResult, query, ttl);
}, 3000);
}
})
}
QueryResult.getById = function (id) {
var queryResult = new QueryResult();
QueryResultResource.get({'id': id}, function (response) {
queryResult.update(response);
});
return queryResult;
};
QueryResult.prototype.toPromise = function() {
return this.deferred.promise;
}
QueryResult.get = function (data_source_id, query, ttl) {
var queryResult = new QueryResult();
QueryResultResource.post({'data_source_id': data_source_id, 'query': query, 'ttl': ttl}, function (response) {
queryResult.update(response);
if ('job' in response) {
refreshStatus(queryResult, query, ttl);
}
});
return queryResult;
}
return QueryResult;
};
var Query = function ($resource, QueryResult, DataSource) {
var Query = $resource('/api/queries/:id', {id: '@id'});
Query.newQuery = function () {
return new Query({
query: "",
name: "New Query",
ttl: -1,
user: currentUser
});
};
Query.prototype.getSourceLink = function () {
return '/queries/' + this.id + '/source';
};
Query.prototype.getQueryResult = function (ttl) {
if (ttl == undefined) {
ttl = this.ttl;
}
if (this.latest_query_data && ttl != 0) {
if (!this.queryResult) {
this.queryResult = new QueryResult({'query_result': this.latest_query_data});
}
} else if (this.latest_query_data_id && ttl != 0) {
if (!this.queryResult) {
this.queryResult = QueryResult.getById(this.latest_query_data_id);
}
} else if (this.data_source_id) {
this.queryResult = QueryResult.get(this.data_source_id, this.query, ttl);
}
return this.queryResult;
};
Query.prototype.getQueryResultPromise = function() {
return this.getQueryResult().toPromise();
}
return Query;
};
var DataSource = function ($resource) {
var DataSourceResource = $resource('/api/data_sources/:id', {id: '@id'}, {'get': {'method': 'GET', 'cache': true, 'isArray': true}});
return DataSourceResource;
}
var Widget = function ($resource, Query) {
var WidgetResource = $resource('/api/widgets/:id', {id: '@id'});
WidgetResource.prototype.getQuery = function () {
if (!this.query && this.visualization) {
this.query = new Query(this.visualization.query);
}
return this.query;
};
WidgetResource.prototype.getName = function () {
if (this.visualization) {
return this.visualization.query.name + ' (' + this.visualization.name + ')';
}
return _.str.truncate(this.text, 20);
};
return WidgetResource;
}
angular.module('redash.services')
.factory('QueryResult', ['$resource', '$timeout', '$q', QueryResult])
.factory('Query', ['$resource', 'QueryResult', 'DataSource', Query])
.factory('DataSource', ['$resource', DataSource])
.factory('Widget', ['$resource', 'Query', Widget]);
})();

View File

@@ -0,0 +1,52 @@
(function () {
'use strict'
function KeyboardShortcuts() {
this.bind = function bind(keymap) {
_.forEach(keymap, function (fn, key) {
Mousetrap.bindGlobal(key, function (e) {
e.preventDefault();
fn();
});
});
}
this.unbind = function unbind(keymap) {
_.forEach(keymap, function (fn, key) {
Mousetrap.unbind(key);
});
}
}
function Events($http) {
this.events = [];
this.post = _.debounce(function() {
var events = this.events;
this.events = [];
$http.post('/api/events', events);
}, 1000);
this.record = function (user, action, object_type, object_id, additional_properties) {
var event = {
"user_id": user.id,
"action": action,
"object_type": object_type,
"object_id": object_id,
"timestamp": Date.now()/1000.0
};
_.extend(event, additional_properties);
this.events.push(event);
this.post();
};
}
angular.module('redash.services', [])
.service('KeyboardShortcuts', [KeyboardShortcuts])
.service('Events', ['$http', Events])
})();

View File

@@ -0,0 +1,213 @@
(function () {
var VisualizationProvider = function () {
this.visualizations = {};
this.visualizationTypes = {};
var defaultConfig = {
defaultOptions: {},
skipTypes: false,
editorTemplate: null
}
this.registerVisualization = function (config) {
var visualization = _.extend({}, defaultConfig, config);
// TODO: this is prone to errors; better refactor.
if (_.isEmpty(this.visualizations)) {
this.defaultVisualization = visualization;
}
this.visualizations[config.type] = visualization;
if (!config.skipTypes) {
this.visualizationTypes[config.name] = config.type;
}
;
};
this.getSwitchTemplate = function (property) {
var pattern = /(<[a-zA-Z0-9-]*?)( |>)/
var mergedTemplates = _.reduce(this.visualizations, function (templates, visualization) {
if (visualization[property]) {
var ngSwitch = '$1 ng-switch-when="' + visualization.type + '" $2';
var template = visualization[property].replace(pattern, ngSwitch);
return templates + "\n" + template;
}
return templates;
}, "");
mergedTemplates = '<div ng-switch on="visualization.type">' + mergedTemplates + "</div>";
return mergedTemplates;
}
this.$get = ['$resource', function ($resource) {
var Visualization = $resource('/api/visualizations/:id', {id: '@id'});
Visualization.visualizations = this.visualizations;
Visualization.visualizationTypes = this.visualizationTypes;
Visualization.renderVisualizationsTemplate = this.getSwitchTemplate('renderTemplate');
Visualization.editorTemplate = this.getSwitchTemplate('editorTemplate');
Visualization.defaultVisualization = this.defaultVisualization;
return Visualization;
}];
};
var VisualizationRenderer = function ($location, Visualization) {
return {
restrict: 'E',
scope: {
visualization: '=',
queryResult: '='
},
// TODO: using switch here (and in the options editor) might introduce errors and bad
// performance wise. It's better to eventually show the correct template based on the
// visualization type and not make the browser render all of them.
template: '<filters></filters>\n' + Visualization.renderVisualizationsTemplate,
replace: false,
link: function (scope) {
scope.select2Options = {
width: '50%'
};
function readURL() {
var searchFilters = angular.fromJson($location.search().filters);
if (searchFilters) {
_.forEach(scope.filters, function(filter) {
var value = searchFilters[filter.friendlyName];
if (value) {
filter.current = value;
}
});
}
}
function updateURL(filters) {
var current = {};
_.each(filters, function(filter) {
if (filter.current) {
current[filter.friendlyName] = filter.current;
}
});
var newSearch = angular.extend($location.search(), {
filters: angular.toJson(current)
});
$location.search(newSearch);
}
scope.$watch('queryResult && queryResult.getFilters()', function (filters) {
if (filters) {
scope.filters = filters;
if (filters.length && false) {
readURL();
// start watching for changes and update URL
scope.$watch('filters', updateURL, true);
}
}
});
}
}
};
var VisualizationOptionsEditor = function (Visualization) {
return {
restrict: 'E',
template: Visualization.editorTemplate,
replace: false
}
};
var Filters = function () {
return {
restrict: 'E',
templateUrl: '/views/visualizations/filters.html'
}
}
var EditVisualizationForm = function (Events, Visualization, growl) {
return {
restrict: 'E',
templateUrl: '/views/visualizations/edit_visualization.html',
replace: true,
scope: {
query: '=',
queryResult: '=',
visualization: '=?',
openEditor: '=?',
onNewSuccess: '=?'
},
link: function (scope, element, attrs) {
scope.editRawOptions = currentUser.hasPermission('edit_raw_chart');
scope.visTypes = Visualization.visualizationTypes;
scope.newVisualization = function () {
return {
'type': Visualization.defaultVisualization.type,
'name': Visualization.defaultVisualization.name,
'description': '',
'options': Visualization.defaultVisualization.defaultOptions
};
}
if (!scope.visualization) {
var unwatch = scope.$watch('query.id', function (queryId) {
if (queryId) {
unwatch();
scope.visualization = scope.newVisualization();
}
});
}
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) {
// poor man's titlecase
scope.visualization.name = scope.visualization.type[0] + scope.visualization.type.slice(1).toLowerCase();
}
});
scope.submit = function () {
if (scope.visualization.id) {
Events.record(currentUser, "update", "visualization", scope.visualization.id, {'type': scope.visualization.type});
} else {
Events.record(currentUser, "create", "visualization", null, {'type': scope.visualization.type});
}
scope.visualization.query_id = scope.query.id;
Visualization.save(scope.visualization, function success(result) {
growl.addSuccessMessage("Visualization saved");
scope.visualization = scope.newVisualization(scope.query);
var visIds = _.pluck(scope.query.visualizations, 'id');
var index = visIds.indexOf(result.id);
if (index > -1) {
scope.query.visualizations[index] = result;
} else {
// new visualization
scope.query.visualizations.push(result);
scope.onNewSuccess && scope.onNewSuccess(result);
}
}, function error() {
growl.addErrorMessage("Visualization could not be saved");
});
};
}
}
};
angular.module('redash.visualization', [])
.provider('Visualization', VisualizationProvider)
.directive('visualizationRenderer', ['$location', 'Visualization', VisualizationRenderer])
.directive('visualizationOptionsEditor', ['Visualization', VisualizationOptionsEditor])
.directive('filters', Filters)
.directive('editVisulatizationForm', ['Events', 'Visualization', 'growl', EditVisualizationForm])
})();

View File

@@ -0,0 +1,244 @@
(function () {
var chartVisualization = angular.module('redash.visualization');
chartVisualization.config(['VisualizationProvider', function (VisualizationProvider) {
var renderTemplate = '<chart-renderer options="visualization.options" query-result="queryResult"></chart-renderer>';
var editTemplate = '<chart-editor></chart-editor>';
var defaultOptions = {
'series': {
// 'type': 'column',
'stacking': null
}
};
VisualizationProvider.registerVisualization({
type: 'CHART',
name: 'Chart',
renderTemplate: renderTemplate,
editorTemplate: editTemplate,
defaultOptions: defaultOptions
});
}]);
chartVisualization.directive('chartRenderer', function () {
return {
restrict: 'E',
scope: {
queryResult: '=',
options: '=?'
},
template: "<chart options='chartOptions' series='chartSeries' class='graph'></chart>",
replace: false,
controller: ['$scope', function ($scope) {
$scope.chartSeries = [];
$scope.chartOptions = {};
var reloadData = function(data) {
if (!data || ($scope.queryResult && $scope.queryResult.getData()) == null) {
$scope.chartSeries.splice(0, $scope.chartSeries.length);
} else {
$scope.chartSeries.splice(0, $scope.chartSeries.length);
_.each($scope.queryResult.getChartData($scope.options.columnMapping), function (s) {
var additional = {'stacking': 'normal'};
if ($scope.options.seriesOptions && $scope.options.seriesOptions[s.name]) {
additional = $scope.options.seriesOptions[s.name];
if (!additional.name || additional.name == "") {
additional.name = s.name;
}
}
$scope.chartSeries.push(_.extend(s, additional));
});
};
};
$scope.$watch('options', function (chartOptions) {
if (chartOptions) {
$scope.chartOptions = chartOptions;
}
});
$scope.$watch('options.seriesOptions', function () {
reloadData(true);
}, true);
$scope.$watchCollection('options.columnMapping', function (chartOptions) {
reloadData(true);
});
$scope.$watch('queryResult && queryResult.getData()', function (data) {
reloadData(data);
});
}]
};
});
chartVisualization.directive('chartEditor', function () {
return {
restrict: 'E',
templateUrl: '/views/visualizations/chart_editor.html',
link: function (scope, element, attrs) {
scope.seriesTypes = {
'Line': 'line',
'Column': 'column',
'Area': 'area',
'Scatter': 'scatter',
'Pie': 'pie'
};
scope.globalSeriesType = 'column';
scope.stackingOptions = {
"None": "none",
"Normal": "normal",
"Percent": "percent"
};
scope.xAxisOptions = {
"Date/Time": "datetime",
"Linear": "linear",
"Category": "category"
};
scope.xAxisType = "datetime";
scope.stacking = "none";
scope.columnTypes = {
"X": "x",
// "X (Date time)": "x",
// "X (Linear)": "x-linear",
// "X (Category)": "x-category",
"Y": "y",
"Series": "series",
"Unused": "unused"
};
scope.series = [];
scope.columnTypeSelection = {};
var chartOptionsUnwatch = null,
columnsWatch = null;
scope.$watch('globalSeriesType', function(type, old) {
if (type && old && type !== old && scope.visualization.options.seriesOptions) {
_.each(scope.visualization.options.seriesOptions, function(sOptions) {
sOptions.type = type;
});
}
});
scope.$watch('visualization.type', function (visualizationType) {
if (visualizationType == 'CHART') {
if (scope.visualization.options.series.stacking === null) {
scope.stacking = "none";
} else if (scope.visualization.options.series.stacking === undefined) {
scope.stacking = "normal";
} else {
scope.stacking = scope.visualization.options.series.stacking;
}
var refreshSeries = function() {
scope.series = _.map(scope.queryResult.getChartData(scope.visualization.options.columnMapping), function (s) { return s.name; });
// TODO: remove uneeded ones?
if (scope.visualization.options.seriesOptions == undefined) {
scope.visualization.options.seriesOptions = {
type: scope.globalSeriesType
};
};
_.each(scope.series, function(s, i) {
if (scope.visualization.options.seriesOptions[s] == undefined) {
scope.visualization.options.seriesOptions[s] = {'type': 'column', 'yAxis': 0};
}
scope.visualization.options.seriesOptions[s].zIndex = i;
});
scope.zIndexes = _.range(scope.series.length);
scope.yAxes = [[0, 'left'], [1, 'right']];
};
var initColumnMapping = function() {
scope.columns = scope.queryResult.getColumns();
if (scope.visualization.options.columnMapping == undefined) {
scope.visualization.options.columnMapping = {};
}
scope.columnTypeSelection = scope.visualization.options.columnMapping;
_.each(scope.columns, function(column) {
var definition = column.name.split("::"),
definedColumns = _.keys(scope.visualization.options.columnMapping);
if (_.indexOf(definedColumns, column.name) != -1) {
// Skip already defined columns.
return;
};
if (definition.length == 1) {
scope.columnTypeSelection[column.name] = scope.visualization.options.columnMapping[column.name] = 'unused';
} else if (definition == 'multi-filter') {
scope.columnTypeSelection[column.name] = scope.visualization.options.columnMapping[column.name] = 'series';
} else if (_.indexOf(_.values(scope.columnTypes), definition[1]) != -1) {
scope.columnTypeSelection[column.name] = scope.visualization.options.columnMapping[column.name] = definition[1];
} else {
scope.columnTypeSelection[column.name] = scope.visualization.options.columnMapping[column.name] = 'unused';
}
});
};
columnsWatch = scope.$watch('queryResult.getId()', function(id) {
if (!id) {
return;
}
initColumnMapping();
refreshSeries();
});
scope.$watchCollection('columnTypeSelection', function(selections) {
_.each(scope.columnTypeSelection, function(type, name) {
scope.visualization.options.columnMapping[name] = type;
});
refreshSeries();
});
chartOptionsUnwatch = scope.$watch("stacking", function (stacking) {
if (stacking == "none") {
scope.visualization.options.series.stacking = null;
} else {
scope.visualization.options.series.stacking = stacking;
}
});
scope.xAxisType = (scope.visualization.options.xAxis && scope.visualization.options.xAxis.type) || scope.xAxisType;
xAxisUnwatch = scope.$watch("xAxisType", function (xAxisType) {
scope.visualization.options.xAxis = scope.visualization.options.xAxis || {};
scope.visualization.options.xAxis.type = xAxisType;
});
} else {
if (chartOptionsUnwatch) {
chartOptionsUnwatch();
chartOptionsUnwatch = null;
}
if (columnsWatch) {
columnWatch();
columnWatch = null;
}
if (xAxisUnwatch) {
xAxisUnwatch();
xAxisUnwatch = null;
}
}
});
}
}
});
}());

View File

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

View File

@@ -0,0 +1,29 @@
var renderers = angular.module('redash.renderers', []);
renderers.directive('pivotTableRenderer', function () {
return {
restrict: 'E',
scope: {
queryResult: '='
},
template: "",
replace: false,
link: function($scope, element, attrs) {
$scope.$watch('queryResult && queryResult.getData()', function (data) {
if (!data) {
return;
}
if ($scope.queryResult.getData() == null) {
} else {
// We need to give the pivot table its own copy of the data, because its change
// it which interferes with other visualizations.
var data = $.extend(true, [], $scope.queryResult.getData());
$(element).pivotUI(data, {
renderers: $.pivotUtilities.renderers
}, true);
}
});
}
}
});

View File

@@ -0,0 +1,109 @@
(function () {
var tableVisualization = angular.module('redash.visualization');
tableVisualization.config(['VisualizationProvider', function (VisualizationProvider) {
VisualizationProvider.registerVisualization({
type: 'TABLE',
name: 'Table',
renderTemplate: '<grid-renderer options="visualization.options" query-result="queryResult"></grid-renderer>',
skipTypes: true
});
}]);
tableVisualization.directive('gridRenderer', function () {
return {
restrict: 'E',
scope: {
queryResult: '=',
itemsPerPage: '='
},
templateUrl: "/views/grid_renderer.html",
replace: false,
controller: ['$scope', '$filter', function ($scope, $filter) {
$scope.gridColumns = [];
$scope.gridData = [];
$scope.gridConfig = {
isPaginationEnabled: true,
itemsByPage: $scope.itemsPerPage || 15,
maxSize: 8
};
$scope.$watch('queryResult && queryResult.getData()', function (data) {
if (!data) {
return;
}
if ($scope.queryResult.getData() == null) {
$scope.gridColumns = [];
$scope.gridData = [];
$scope.filters = [];
} else {
$scope.filters = $scope.queryResult.getFilters();
var prepareGridData = function (data) {
var gridData = _.map(data, function (row) {
var newRow = {};
_.each(row, function (val, key) {
newRow[$scope.queryResult.getColumnCleanName(key)] = val;
})
return newRow;
});
return gridData;
};
$scope.gridData = prepareGridData($scope.queryResult.getData());
var columns = $scope.queryResult.getColumns();
$scope.gridColumns = _.map($scope.queryResult.getColumnCleanNames(), function (col, i) {
var columnDefinition = {
'label': $scope.queryResult.getColumnFriendlyNames()[i],
'map': col
};
var columnType = columns[i].type;
if (columnType === 'integer') {
columnDefinition.formatFunction = 'number';
columnDefinition.formatParameter = 0;
} else if (columnType === 'float') {
columnDefinition.formatFunction = 'number';
columnDefinition.formatParameter = 2;
} else if (columnType === 'boolean') {
columnDefinition.formatFunction = function (value) {
if (value !== undefined) {
return "" + value;
}
return value;
};
} else if (columnType === 'date') {
columnDefinition.formatFunction = function (value) {
if (value) {
return value.format("DD/MM/YY");
}
return value;
};
} else if (columnType === 'datetime') {
columnDefinition.formatFunction = function (value) {
if (value) {
return value.format("DD/MM/YY HH:mm");
}
return value;
};
} else {
columnDefinition.formatFunction = function (value) {
if (angular.isString(value)) {
value = $filter('linkify')(value);
}
return value;
}
}
return columnDefinition;
});
}
});
}]
}
})
}());

View File

@@ -0,0 +1,37 @@
.main {
max-width: 320px;
margin: 0 auto;
}
.login-or {
position: relative;
font-size: 18px;
color: #aaa;
margin-top: 10px;
margin-bottom: 10px;
padding-top: 10px;
padding-bottom: 10px;
}
.span-or {
display: block;
position: absolute;
left: 50%;
top: -2px;
margin-left: -25px;
background-color: #fff;
width: 50px;
text-align: center;
}
.hr-or {
background-color: #cdcdcd;
height: 1px;
margin-top: 0px !important;
margin-bottom: 0px !important;
}
/*h3 {*/
/*text-align: center;*/
/*line-height: 300%;*/
/*}*/

View File

@@ -25,7 +25,33 @@ a.navbar-brand {
margin-top: 5px;
margin-bottom: 5px;
}
.avatar img {
width: 40px;
height: 40px;
}
#logout {
color: white;
position: relative;
left: -9px;
bottom: -11px;
}
.details-toggle {
cursor: pointer;
}
.details-toggle::before {
content: '▸';
margin-right: 5px;
}
.details-toggle.open::before {
content: '▾';
margin-right: 5px;
}
.edit-in-place span {
white-space: pre-line;
}
.edit-in-place span.editable {
cursor: pointer;
}
@@ -60,10 +86,15 @@ a.navbar-brand {
margin-bottom: 0px;
}
.panel-heading > a {
.panel-heading > a,
.panel-heading .query-link {
color: inherit;
}
.panel-heading .query-link:hover {
text-decoration: none;
}
/* angular-growl */
.growl {
position: fixed;
@@ -213,4 +244,47 @@ to add those CSS styles here. */
color: white;
background-color: #FF8080;
border-radius: 50%;
}
}
.nav-tabs > li.rd-tab-btn {
float: right;
}
/* light version of bootstrap's form-control */
.rd-form-control {
display: block;
padding: 6px 12px;
line-height: 1.428571429;
color: #555555;
vertical-align: middle;
background-color: #ffffff;
border: 1px solid #cccccc;
border-radius: 4px;
-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 0.15s, box-shadow ease-in-out 0.15s;
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
}
.rd-form-control {
width: 100%;
}
pivot-table-renderer > table, grid-renderer > div, visualization-renderer > div {
overflow: auto;
}
.rd-widget-textbox p {
margin-bottom: 0;
}
.iframe-container {
height: 100%;
}
/*
bootstrap's hidden-xs class adds display:block when not hidden
use this class when you need to keep the original display value
*/
@media (max-width: 767px) {
.rd-hidden-xs {
display: none !important;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 845 B

View File

@@ -21,30 +21,20 @@
Started
</li>
<li class="list-group-item">
<span class="badge">{{manager.queue_size}}</span>
Queue Size
<span class="badge">{{manager.outdated_queries_count}}</span>
Outdated Queries Count
</li>
<li class="list-group-item" ng-if="flowerUrl">
<a href="/admin/workers">Workers' Status</a>
</li>
</ul>
<ul class="list-group col-lg-4">
<div ng-repeat="worker in workers">
<li class="list-group-item active">Worker {{$index+1}}</li>
<li class="list-group-item">
<span class="badge" am-time-ago="worker.updated_at*1000.0"></span>
Updated
<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}})
</li>
<li class="list-group-item">
<span class="badge" am-time-ago="worker.started_at*1000.0"></span>
Started
</li>
<li class="list-group-item">
<span class="badge">{{worker.jobs_count}}</span>
Jobs Received
</li>
<li class="list-group-item">
<span class="badge">{{worker.done_jobs_count}}</span>
Jobs Done
</li>
</div>
</ul>
</div>
<div class="panel-footer">Next refresh: <span am-time-ago="refresh_time"></span></div>

View File

@@ -0,0 +1,3 @@
<div class="container-fluid iframe-container">
<iframe src="{{flowerUrl}}" style="width:100%; height:100%; background-color:transparent;"></iframe>
</div>

View File

@@ -4,6 +4,8 @@
<div class="container">
<h2 id="dashboard_title">
{{dashboard.name}}
<button type="button" class="btn btn-default btn-xs" ng-class="{active: refreshEnabled}" tooltip="Enable/Disable Auto Refresh" ng-click="triggerRefresh()"><span class="glyphicon glyphicon-refresh"></span></button>
<span ng-show="dashboard.canEdit()">
<button type="button" class="btn btn-default btn-xs" data-toggle="modal" href="#edit_dashboard_dialog" tooltip="Edit Dashboard (Name/Layout)"><span
class="glyphicon glyphicon-cog"></span></button>
@@ -12,6 +14,7 @@
</button>
</span>
</h2>
<filters></filters>
</div>
<div class="container" id="dashboard">
@@ -19,11 +22,12 @@
<div ng-repeat="widget in row" class="col-lg-{{widget.width | colWidth}}"
ng-controller='WidgetCtrl'>
<div class="panel panel-default">
<div class="panel panel-default" ng-if="type=='visualization'">
<div class="panel-heading">
<h3 class="panel-title" style="cursor: pointer;" ng-click="open(query)">
<h3 class="panel-title">
<p>
<span ng-bind="query.name"></span>
<span ng-hide="currentUser.hasPermission('view_query')">{{query.name}}</span>
<query-link query="query" visualization="widget.visualization" ng-show="currentUser.hasPermission('view_query')"></query-link>
</p>
<div class="text-muted" ng-bind="query.description"></div>
</h3>
@@ -37,12 +41,32 @@
tooltip-placement="bottom">Updated: <span am-time-ago="queryResult.getUpdatedAt()"></span></span>
<span class="pull-right">
<a class="btn btn-default btn-xs" ng-href="/queries/{{query.id}}"><span class="glyphicon glyphicon-link"></span></a>
<a class="btn btn-default btn-xs" ng-href="/queries/{{query.id}}#{{widget.visualization.id}}" ng-show="currentUser.hasPermission('view_query')"><span class="glyphicon glyphicon-link"></span></a>
<button type="button" class="btn btn-default btn-xs" ng-show="dashboard.canEdit()" ng-click="deleteWidget()" title="Remove Widget"><span class="glyphicon glyphicon-trash"></span></button>
</span>
<span class="pull-right">
<a class="btn btn-default btn-xs" ng-disabled="!queryResult.getData()" query-result-link target="_self">
<span class="glyphicon glyphicon-cloud-download"></span>
</a>
</span>
</div>
</div>
<div class="panel panel-default rd-widget-textbox" ng-if="type=='textbox'" ng-mouseenter="showControls = true" ng-mouseleave="showControls = false">
<div class="panel-body">
<div class="row">
<div class="col-lg-11">
<p ng-bind-html="widget.text | markdown"></p>
</div>
<div class="col-lg-1">
<span class="pull-right" ng-show="showControls">
<button type="button" class="btn btn-default btn-xs" ng-show="dashboard.canEdit()" ng-click="deleteWidget()" title="Remove Widget"><span class="glyphicon glyphicon-trash"></span></button>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,31 +0,0 @@
<form role="form" name="visForm" ng-submit="submit()">
<div class="form-group">
<label class="control-label">Name</label>
<input type="text" class="form-control" ng-model="vis.name" placeholder="{{query.name}}">
</div>
<div class="form-group">
<label class="control-label">Description</label>
<textarea class="form-control" ng-model="vis.description" placeholder="{{query.description}}"></textarea>
</div>
<div class="form-group">
<label class="control-label">Visualization Type</label>
<select required ng-model="vis.type" ng-options="value as key for (key, value) in visTypes" class="form-control" ng-change="typeChanged()"></select>
</div>
<div class="form-group" ng-show="vis.type == visTypes.Chart">
<label class="control-label">Chart Type</label>
<select required ng-model="vis.options.series.type" ng-options="value as key for (key, value) in seriesTypes" class="form-control"></select>
</div>
<div class="form-group">
<a class="link" ng-click="toggleAdvancedMode()">Advanced</a>
<textarea json-text class="form-control" rows="5" ng-model="vis.options" ng-show="advancedMode"></textarea>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>

View File

@@ -1,17 +1,4 @@
<div>
<div class="btn-group pull-right" ng-repeat="filter in filters">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
{{filter.friendlyName}}: {{filter.current}}<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li ng-repeat="value in filter.values">
<a href="#" ng-click="filter.current = value">{{value}}</a>
</li>
<li class="divider"></li>
<li><a href="#" ng-click="filter.current = 'All'">All</a></li>
</ul>
</div>
<smart-table rows="gridData" columns="gridColumns"
config="gridConfig"
class="table table-condensed table-hover"></smart-table>

View File

@@ -3,7 +3,7 @@
<div class="list-group" ng-repeat="(name, dashboards) in allDashboards">
<div class="list-group-item active">
{{name}}
<button type="button" class="btn btn-sm btn-link" data-toggle="modal" href="#new_dashboard_dialog" tooltip="New Dashboard"><span class="glyphicon glyphicon-plus-sign"></span></button>
<button ng-show="currentUser.hasPermission('create_dashboard')" type="button" class="btn btn-sm btn-link" data-toggle="modal" href="#new_dashboard_dialog" tooltip="New Dashboard"><span class="glyphicon glyphicon-plus-sign"></span></button>
</div>
<div class="list-group-item" ng-repeat="dashboard in dashboards" >
<button type="button" class="close delete-button" aria-hidden="true" ng-show="dashboard.canEdit()" ng-click="archiveDashboard(dashboard)" tooltip="Delete Dashboard">&times;</button>
@@ -11,7 +11,7 @@
</div>
</div>
<div ng-show="currentUser.is_admin">
<div ng-show="currentUser.hasPermission('admin')">
<div class="list-group">
<div class="list-group-item active">Admin</div>
<a href="/admin/status" class="list-group-item">Status</a>

View File

@@ -6,42 +6,54 @@
<h4 class="modal-title">Add Widget</h4>
</div>
<div class="modal-body">
<p>
<form class="form-inline" role="form" ng-submit="loadVisualizations()">
<div class="form-group">
<input class="form-control" placeholder="Query Id" ng-model="queryId">
</div>
<button type="submit" class="btn btn-primary" ng-disabled="!queryId">
<span class="glyphicon glyphicon-refresh"></span> Load
</button>
</form>
<p class="btn-group">
<button type="button" class="btn btn-default" ng-class="{active: isVisualization()}" ng-click="setType('visualization')">Visualization</button>
<button type="button" class="btn btn-default" ng-class="{active: isTextBox()}" ng-click="setType('textbox')">Text Box</button>
</p>
<div ng-show="query">
<div class="form-group">
<label for="">Choose Visualation</label>
<select ng-model="selectedVis" ng-options="vis as vis.name group by vis.type for vis in query.visualizations" class="form-control"></select>
</div>
<div class="form-group">
<a ng-click="toggleView('addNew')" class="link">&plus; Add New</a>
<div class="well" ng-show="currentView=='addNew'">
<edit-visulatization-form query="query"></edit-visulatization-form>
<div ng-show="isTextBox()">
<div class="form-group">
<textarea class="form-control" ng-model="text" rows="3"></textarea>
</div>
</div>
<div ng-show="text">
<strong>Preview:</strong>
<p ng-bind-html="text | markdown"></p>
</div>
</div>
<div class="form-group">
<label for="">Widget Size</label>
<select class="form-control" ng-model="widgetSize" ng-options="c.value as c.name for c in widgetSizes"></select>
</div>
<div ng-show="isVisualization()">
<p>
<form class="form-inline" role="form" ng-submit="loadVisualizations()">
<div class="form-group">
<input class="form-control" placeholder="Query Id" ng-model="queryId">
</div>
<button type="submit" class="btn btn-primary" ng-disabled="!queryId">
Load visualizations
</button>
</form>
</p>
<div ng-show="query">
<div class="form-group">
<label for="">Choose Visualization</label>
<select ng-model="selectedVis" ng-options="vis as vis.name group by vis.type for vis in query.visualizations" class="form-control"></select>
</div>
</div>
</div>
<div class="form-group">
<label for="">Widget Size</label>
<select class="form-control" ng-model="widgetSize"
ng-options="c.value as c.name for c in widgetSizes"></select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" ng-disabled="saveInProgress" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" ng-disabled="saveInProgress" ng-click="saveWidget()">Add to Dashboard</button>
<button type="button" class="btn btn-primary" ng-disabled="saveInProgress || !(selectedVis || isTextBox())" ng-click="saveWidget()">Add to Dashboard</button>
</div>
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
</div>

183
rd_ui/app/views/query.html Normal file
View File

@@ -0,0 +1,183 @@
<div class="container">
<alert-unsaved-changes ng-if="canEdit" is-dirty="isDirty"></alert-unsaved-changes>
<div class="row">
<div class="col-lg-12">
<div class="row">
<div class="col-lg-10">
<h2>
<edit-in-place editable="isQueryOwner" done="saveName" ignore-blanks='true' value="query.name"></edit-in-place>
</h2>
<p>
<em>
<edit-in-place editable="isQueryOwner" done="saveDescription" editor="textarea" placeholder="No description" ignore-blanks='false' value="query.description"></edit-in-place>
</em>
</p>
</div>
<div class="col-lg-2">
<div class="rd-hidden-xs pull-right">
<query-source-link></query-source-link>
</div>
</div>
</div>
<div class="visible-xs">
<p>
<span class="text-muted">Last update </span>
<strong>
<rd-time-ago value="queryResult.query_result.retrieved_at"></rd-time-ago>
</strong>
&nbsp;
<span class="text-muted">Created By </span>
<strong ng-hide="isQueryOwner">{{query.user.name}}</strong>
<strong ng-show="isQueryOwner">You</strong>
&nbsp;
<span class="text-muted">Runtime </span>
<strong ng-show="!queryExecuting">{{queryResult.getRuntime() | durationHumanize}}</strong>
<span ng-show="queryExecuting">Running&hellip;</span>
&nbsp;
<span class="text-muted">Rows </span>
<strong>{{queryResult.getData().length}}</strong>
</p>
<p>
<query-source-link></query-source-link>
</p>
</div>
</div>
</div>
<hr>
<div class="row">
<div class="col-lg-12">
<div ng-show="sourceMode">
<p>
<button type="button" class="btn btn-primary btn-xs" ng-disabled="queryExecuting" ng-click="executeQuery()">
<span class="glyphicon glyphicon-play"></span> Execute
</button>
<query-formatter></query-formatter>
<span class="pull-right">
<button class="btn btn-xs btn-default rd-hidden-xs" ng-click="duplicateQuery()">
<span class="glyphicon glyphicon-share-alt"></span> Fork
</button>
<button class="btn btn-success btn-xs" ng-show="canEdit" ng-click="saveQuery()">
<span class="glyphicon glyphicon-floppy-disk"> </span> Save<span ng-show="isDirty">&#42;</span>
</button>
</span>
</p>
</div>
<!-- code editor -->
<div ng-show="sourceMode">
<p>
<query-editor query="query" lock="queryExecuting"></query-editor>
</p>
<hr>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-3 rd-hidden-xs">
<p>
<span class="glyphicon glyphicon-time"></span>
<span class="text-muted">Last update </span>
<strong>
<rd-time-ago value="queryResult.query_result.retrieved_at"></rd-time-ago>
</strong>
</p>
<p>
<span class="glyphicon glyphicon-user"></span>
<span class="text-muted">Created By </span>
<strong ng-hide="isQueryOwner">{{query.user.name}}</strong>
<strong ng-show="isQueryOwner">You</strong>
</p>
<p>
<span class="glyphicon glyphicon-play"></span>
<span class="text-muted">Runtime </span>
<strong ng-show="!queryExecuting">{{queryResult.getRuntime() | durationHumanize}}</strong>
<span ng-show="queryExecuting">Running&hellip;</span>
</p>
<p>
<span class="glyphicon glyphicon-align-justify"></span>
<span class="text-muted">Rows </span><strong>{{queryResult.getData().length}}</strong>
</p>
<p>
<span class="glyphicon glyphicon-refresh"></span>
<span class="text-muted">Refresh Interval</span>
<query-refresh-select></query-refresh-select>
</p>
<p>
<span class="glyphicon glyphicon-hdd"></span>
<span class="text-muted">Data Source</span>
<select ng-disabled="!isQueryOwner" ng-model="query.data_source_id" ng-change="updateDataSource()" ng-options="ds.id as ds.name for ds in dataSources"></select>
</p>
<hr>
<p>
<a class="btn btn-primary btn-sm" ng-disabled="queryExecuting || !queryResult.getData()" query-result-link target="_self">
<span class="glyphicon glyphicon-cloud-download"></span>
<span class="rd-hidden-xs">Download Dataset</span>
</a>
</p>
</div>
<div class="col-lg-9">
<!-- alerts -->
<div class="alert alert-info" ng-show="queryResult.getStatus() == 'processing'">
Executing query&hellip; <rd-timer timestamp="queryResult.getUpdatedAt()"></rd-timer>
<button type="button" class="btn btn-warning btn-xs pull-right" ng-disabled="cancelling" ng-click="cancelExecution()">Cancel</button>
</div>
<div class="alert alert-info" ng-show="queryResult.getStatus() == 'waiting'">
Query in queue&hellip; <rd-timer timestamp="queryResult.getUpdatedAt()"></rd-timer>
<button type="button" class="btn btn-warning btn-xs pull-right" ng-disabled="cancelling" ng-click="cancelExecution()">Cancel</button>
</div>
<div class="alert alert-danger" ng-show="queryResult.getError()">Error running query: <strong>{{queryResult.getError()}}</strong></div>
<!-- tabs and data -->
<div ng-show="showDataset">
<div class="row">
<div class="col-lg-12">
<ul class="nav nav-tabs">
<rd-tab tab-id="table" name="Table"></rd-tab>
<rd-tab tab-id="pivot" name="Pivot Table"></rd-tab>
<rd-tab tab-id="{{vis.id}}" name="{{vis.name}}" ng-if="vis.type!='TABLE'" ng-repeat="vis in query.visualizations">
<span class="remove" ng-click="deleteVisualization($event, vis)" ng-show="canEdit"> &times;</span>
</rd-tab>
<rd-tab tab-id="add" name="&plus; New" removeable="true" ng-show="canEdit"></rd-tab>
<li ng-if="!sourceMode" class="rd-tab-btn"><button class="btn btn-sm btn-default" ng-click="executeQuery()" ng-disabled="queryExecuting" title="Refresh Dataset"><span class="glyphicon glyphicon-refresh"></span></button></li>
</ul>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<div ng-show="selectedTab == 'table'" >
<filters></filters>
<grid-renderer query-result="queryResult" items-per-page="50"></grid-renderer>
</div>
<pivot-table-renderer ng-show="selectedTab == 'pivot'" query-result="queryResult"></pivot-table-renderer>
<div ng-show="selectedTab == vis.id" ng-repeat="vis in query.visualizations">
<visualization-renderer visualization="vis" query-result="queryResult"></visualization-renderer>
<edit-visulatization-form visualization="vis" query="query" query-result="queryResult" ng-show="canEdit"></edit-visulatization-form>
</div>
<div ng-if="canEdit" ng-show="selectedTab == 'add'">
<visualization-renderer visualization="newVisualization" query-result="queryResult"></visualization-renderer>
<edit-visulatization-form visualization="newVisualization" query="query" query-result="queryResult" ng-show="canEdit" open-editor="true" on-new-success="setVisualizationTab"></edit-visulatization-form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,97 +0,0 @@
<div class="container">
<div class="row">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
<p>
<edit-in-place editable="currentUser.canEdit(query)" ignore-blanks='true' value="query.name"></edit-in-place>
</p>
</h3>
<p>
<edit-in-place editable="currentUser.canEdit(query)" editor="textarea" placeholder="No description" ignore-blanks='false' value="query.description" class="text-muted"></edit-in-place>
</p>
</div>
<div class="panel-body">
<textarea ui-codemirror="editorOptions" ng-model="query.query"></textarea>
<div>
<a class="btn btn-default" ng-disabled="queryExecuting || !queryResult.getData()" ng-href="{{dataUri}}" download="{{dataFilename}}" target="_self">
<span class="glyphicon glyphicon-floppy-disk"></span> Download Data Set
</a>
<button type="button" class="btn btn-default center-x" ng-click="formatQuery()"><span class="glyphicon glyphicon-ok"></span> Format SQL</button>
<div class="btn-group pull-right">
<button type="button" class="btn btn-default" ng-click="duplicateQuery()">Duplicate</button>
<button type="button" class="btn btn-default" ng-disabled="!currentUser.canEdit(query)" ng-click="saveQuery()">Save
<span ng-show="dirty">&#42;</span>
</button>
<button type="button" class="btn btn-primary" ng-disabled="queryExecuting" ng-click="executeQuery()">Execute</button>
</div>
</div>
</div>
<div class="panel-footer">
<span ng-show="queryResult.getRuntime()>=0">Query runtime: {{queryResult.getRuntime() | durationHumanize}} | </span>
<span ng-show="queryResult.query_result.retrieved_at">Last update time: <span am-time-ago="queryResult.query_result.retrieved_at"></span> | </span>
<span ng-show="queryResult.getStatus() == 'done'">Rows: {{queryResult.getData().length}} | </span>
Created by: {{query.user}}
<div class="pull-right">Refresh query: <select ng-model="query.ttl" ng-options="c.value as c.name for c in refreshOptions"></select><br></div>
</div>
</div>
<div class="alert alert-info" ng-show="queryResult.getStatus() == 'processing'">
Executing query... <rd-timer timestamp="queryResult.getUpdatedAt()"></rd-timer>
<button type="button" class="btn btn-warning btn-xs pull-right" ng-disabled="cancelling" ng-click="cancelExecution()">Cancel</button>
</div>
<div class="alert alert-info" ng-show="queryResult.getStatus() == 'waiting'">
Query in queue... <rd-timer timestamp="queryResult.getUpdatedAt()"></rd-timer>
<button type="button" class="btn btn-warning btn-xs pull-right" ng-disabled="cancelling" ng-click="cancelExecution()">Cancel</button>
</div>
<div class="alert alert-danger" ng-show="queryResult.getError()">Error running query: <strong>{{queryResult.getError()}}</strong></div>
</div>
<div class="row" ng-show="queryResult.getStatus() == 'done'">
<ul class="nav nav-tabs">
<rd-tab id="table" name="Table"></rd-tab>
<rd-tab id="pivot" name="Pivot Table"></rd-tab>
<rd-tab id="{{vis.id}}" name="{{vis.name}}" ng-repeat="vis in query.visualizations">
<span class="remove" ng-click="deleteVisualization($event, vis)"> &times;</span>
</rd-tab>
<rd-tab id="add" name="&plus;New" removeable="true"></rd-tab>
</ul>
<div class="col-lg-12" ng-show="selectedTab == 'table'">
<grid-renderer query-result="queryResult" items-per-page="50"></grid-renderer>
</div>
<div class="col-lg-12" ng-show="selectedTab == 'pivot'">
<pivot-table-renderer query-result="queryResult"></pivot-table-renderer>
</div>
<div class="col-lg-12" ng-show="selectedTab == vis.id" ng-repeat="vis in query.visualizations">
<div class="row">
<p>
<div class="col-lg-6">
<edit-visulatization-form vis="vis" query="query"></edit-visulatization-form>
</div>
<div class="col-lg-6">
<visualization-renderer visualization="vis" query-result="queryResult"></visualization-renderer>
</div>
</p>
</div>
</div>
<div class="col-lg-12" ng-show="selectedTab == 'add'">
<div class="row">
<p>
<div class="col-lg-6">
<edit-visulatization-form vis="newVisualization" query="query"></edit-visulatization-form>
</div>
<div class="col-lg-6">
<visualization-renderer visualization="newVisualization" query-result="queryResult"></visualization-renderer>
</div>
</p>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,100 @@
<div class="form-horizontal">
<div class="panel panel-default">
<div class="panel-body">
<div class="form-group">
<label class="control-label col-sm-2">Stacking</label>
<div class="col-sm-10">
<select required ng-model="stacking"
ng-options="value as key for (key, value) in stackingOptions"
class="form-control"></select>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">X Axis Type</label>
<div class="col-sm-10">
<select required ng-model="xAxisType" ng-options="value as key for (key, value) in xAxisOptions"
class="form-control"></select>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">Series Type</label>
<div class="col-sm-10">
<select required ng-options="value as key for (key, value) in seriesTypes"
ng-model="globalSeriesType" class="form-control"></select>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-6">
<div class="list-group">
<div class="list-group-item active">
Columns Mapping
</div>
<div class="list-group-item">
<div class="form-group" ng-repeat="column in columns">
<label class="control-label col-sm-4">{{column.name}}</label>
<div class="col-sm-8">
<select ng-options="value as key for (key, value) in columnTypes" class="form-control"
ng-model="columnTypeSelection[column.name]"></select>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-6" ng-if="series.length > 0">
<div class="list-group" ng-repeat="seriesName in series">
<div class="list-group-item active">
{{seriesName}}
</div>
<div class="list-group-item">
<div class="form-group">
<label class="control-label col-sm-3">Type</label>
<div class="col-sm-9">
<select required ng-model="visualization.options.seriesOptions[seriesName].type"
ng-options="value as key for (key, value) in seriesTypes"
class="form-control"></select>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-3">zIndex</label>
<div class="col-sm-9">
<select required ng-model="visualization.options.seriesOptions[seriesName].zIndex"
ng-options="o as o for o in zIndexes"
class="form-control"></select>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-3">y Axis</label>
<div class="col-sm-9">
<select required ng-model="visualization.options.seriesOptions[seriesName].yAxis"
ng-options="o[0] as o[1] for o in yAxes"
class="form-control"></select>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-3">Name</label>
<div class="col-sm-9">
<input name="seriesName" type="text" class="form-control"
ng-model="visualization.options.seriesOptions[seriesName].name"
placeholder="{{seriesName}}">
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,15 @@
<div class="form-group">
<label class="control-label">Time Label</label>
<input type="text" class="form-control" ng-model="cohortOptions.timeLabel">
<label class="control-label">People Label</label>
<input type="text" class="form-control" ng-model="cohortOptions.peopleLabel">
<label class="control-label">Bucket Column</label>
<select ng-model="bucket_column" ng-options="value as key for (key, value) in columns" class="form-control"></select>
<label class="control-label">Bucket Total Value Column</label>
<select ng-model="total_column" ng-options="value as key for (key, value) in columns" class="form-control"></select>
<label class="control-label">Day Number Column</label>
<select ng-model="value_column" ng-options="value as key for (key, value) in columns" class="form-control"></select>
<label class="control-label">Day Value Column</label>
<select ng-model="day_column" ng-options="value as key for (key, value) in columns" class="form-control"></select>
</div>

View File

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

View File

@@ -0,0 +1,8 @@
<div class="well well-sm" ng-show="filters">
<div ng-repeat="filter in filters">
{{filter.friendlyName}}:
<select ui-select2='select2Options' ng-model="filter.current" ng-multiple="{{filter.multiple}}">
<option ng-repeat="value in filter.values" value="{{value}}">{{value}}</option>
</select>
</div>
</div>

View File

@@ -11,22 +11,27 @@
"moment": "2.1.0",
"angular-ui-bootstrap": "0.5.0",
"angular-ui-codemirror": "0.0.5",
"highcharts": "3.0.1",
"highcharts": "3.0.10",
"underscore": "1.5.1",
"angular-resource": "1.0.7",
"angular-resource": "1.2.15",
"angular-growl": "0.3.1",
"angular-route": "1.2.7",
"pivottable": "git@github.com:arikfr/pivottable.git#master",
"cornelius": "git@github.com:restorando/cornelius.git",
"pivottable": "~1.1.1",
"cornelius": "https://github.com/restorando/cornelius.git",
"gridster": "0.2.0",
"mousetrap": "~1.4.6"
"mousetrap": "~1.4.6",
"angular-ui-select2": "~0.0.5",
"jquery-ui": "~1.10.4",
"underscore.string": "~2.3.3",
"marked": "~0.3.2",
"bucky": "~0.2.6",
"pace": "~0.5.1"
},
"devDependencies": {
"angular-mocks": "~1.0.7",
"angular-scenario": "~1.0.7"
},
"resolutions": {
"angular": "~1.2.7",
"jquery": "~1.9.1"
"angular": "1.2.7"
}
}

View File

@@ -1,58 +0,0 @@
// Karma E2E configuration
// base path, that will be used to resolve files and exclude
basePath = '';
// list of files / patterns to load in the browser
files = [
ANGULAR_SCENARIO,
ANGULAR_SCENARIO_ADAPTER,
'test/e2e/**/*.js'
];
// list of files to exclude
exclude = [];
// test results reporter to use
// possible values: dots || progress || growl
reporters = ['progress'];
// web server port
port = 8080;
// cli runner port
runnerPort = 9100;
// enable / disable colors in the output (reporters and logs)
colors = true;
// level of logging
// possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
logLevel = LOG_INFO;
// enable / disable watching file and executing tests whenever any file changes
autoWatch = false;
// Start these browsers, currently available:
// - Chrome
// - ChromeCanary
// - Firefox
// - Opera
// - Safari (only Mac)
// - PhantomJS
// - IE (only Windows)
browsers = ['Chrome'];
// If browser does not capture in given timeout [ms], kill it
captureTimeout = 5000;
// Continuous Integration mode
// if true, it capture browsers, run tests and exit
singleRun = false;
// Uncomment the following lines if you are using grunt's server to run the tests
// proxies = {
// '/': 'http://localhost:9000/'
// };
// URL root prevent conflicts with the site root
// urlRoot = '_karma_';

View File

@@ -1,56 +0,0 @@
// Karma configuration
// base path, that will be used to resolve files and exclude
basePath = '';
// list of files / patterns to load in the browser
files = [
JASMINE,
JASMINE_ADAPTER,
'app/bower_components/angular/angular.js',
'app/bower_components/angular-mocks/angular-mocks.js',
'app/scripts/*.js',
'app/scripts/**/*.js',
'test/mock/**/*.js',
'test/spec/**/*.js'
];
// list of files to exclude
exclude = [];
// test results reporter to use
// possible values: dots || progress || growl
reporters = ['progress'];
// web server port
port = 8080;
// cli runner port
runnerPort = 9100;
// enable / disable colors in the output (reporters and logs)
colors = true;
// level of logging
// possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
logLevel = LOG_INFO;
// enable / disable watching file and executing tests whenever any file changes
autoWatch = false;
// Start these browsers, currently available:
// - Chrome
// - ChromeCanary
// - Firefox
// - Opera
// - Safari (only Mac)
// - PhantomJS
// - IE (only Windows)
browsers = ['Chrome'];
// If browser does not capture in given timeout [ms], kill it
captureTimeout = 5000;
// Continuous Integration mode
// if true, it capture browsers, run tests and exit
singleRun = false;

View File

@@ -1,38 +1,39 @@
{
"name": "rd-ui",
"version": "0.1.0",
"dependencies": {
},
"name": "rdui",
"version": "0.0.0",
"dependencies": {},
"devDependencies": {
"grunt": "git+https://github.com/gruntjs/grunt.git#08a3af5",
"grunt-contrib-copy": "~0.4.1",
"grunt-contrib-concat": "~0.3.0",
"grunt-contrib-coffee": "~0.7.0",
"grunt-contrib-uglify": "~0.2.0",
"grunt-contrib-compass": "~0.5.0",
"grunt-contrib-jshint": "~0.6.0",
"grunt-contrib-cssmin": "~0.6.0",
"grunt-contrib-connect": "~0.3.0",
"grunt-contrib-clean": "~0.5.0",
"grunt-contrib-htmlmin": "~0.1.3",
"grunt-contrib-imagemin": "~0.2.0",
"grunt-contrib-watch": "~0.5.2",
"grunt-autoprefixer": "~0.2.0",
"grunt-usemin": "~0.1.11",
"grunt-svgmin": "~0.2.0",
"grunt-rev": "~0.1.0",
"grunt-open": "~0.2.0",
"grunt-concurrent": "~0.3.0",
"load-grunt-tasks": "~0.1.0",
"connect-livereload": "~0.2.0",
"grunt-google-cdn": "~0.2.0",
"grunt-ngmin": "~0.0.2",
"time-grunt": "~0.1.0",
"bower": "~1.2.7",
"grunt-cli": "~0.1.9"
"grunt": "^0.4.1",
"grunt-autoprefixer": "^0.7.3",
"grunt-concurrent": "^0.5.0",
"grunt-contrib-clean": "^0.5.0",
"grunt-contrib-concat": "^0.4.0",
"grunt-contrib-connect": "^0.7.1",
"grunt-contrib-copy": "^0.5.0",
"grunt-contrib-cssmin": "^0.9.0",
"grunt-contrib-htmlmin": "^0.3.0",
"grunt-contrib-imagemin": "^0.7.0",
"grunt-contrib-jshint": "^0.10.0",
"grunt-contrib-uglify": "^0.4.0",
"grunt-contrib-watch": "^0.6.1",
"grunt-filerev": "^0.2.1",
"grunt-google-cdn": "^0.4.0",
"grunt-newer": "^0.7.0",
"grunt-ngmin": "^0.0.3",
"grunt-svgmin": "^0.4.0",
"grunt-usemin": "^2.1.1",
"grunt-wiredep": "^1.7.0",
"jshint-stylish": "^0.2.0",
"load-grunt-tasks": "^0.4.0",
"time-grunt": "^0.3.1",
"karma-jasmine": "~0.1.5",
"grunt-karma": "~0.8.3",
"karma-phantomjs-launcher": "~0.1.4",
"karma": "~0.12.19",
"karma-ng-html2js-preprocessor": "~0.1.0"
},
"engines": {
"node": ">=0.8.0"
"node": ">=0.10.0"
},
"scripts": {
"test": "grunt test"

View File

@@ -29,6 +29,7 @@
"expect": false,
"inject": false,
"it": false,
"jasmine": false,
"spyOn": false
}
}

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