Compare commits

...

225 Commits

Author SHA1 Message Date
Arik Fraimovich
900d558857 CirlceCI: Docker build for release branches. 2017-11-02 10:59:21 +02:00
Arik Fraimovich
c6dc9affed Redshift: change default SSL mode to prefer. 2017-11-02 10:57:54 +02:00
Arik Fraimovich
96486b5c58 Update v3 CHANGELOG. 2017-11-02 10:33:53 +02:00
Arik Fraimovich
7c1565017f Merge pull request #2072 from getredash/feature/query_results
🎉  Add: data source to run queries on top of query results
2017-11-01 16:22:32 +02:00
Arik Fraimovich
7197370ad4 Add Query Results to the default query runners list. 2017-11-01 16:14:45 +02:00
Arik Fraimovich
1cbf09cbbe Add: data source to run queries on top of query results. 2017-11-01 16:10:04 +02:00
Arik Fraimovich
28b4450fa9 Merge pull request #2068 from kyoshidajp/copy_param_value_when_forking
Copy parameters value when forking a query
2017-11-01 15:18:22 +02:00
Arik Fraimovich
a799303f53 Merge pull request #2071 from getredash/bugfixes
Fix #1824: allow only user API key to be used with query refresh API.
2017-11-01 15:12:53 +02:00
Arik Fraimovich
59d6eb662c Merge pull request #2070 from getredash/bugfixes
Fix: require full access to the data source to fork a query.
2017-11-01 15:06:36 +02:00
Arik Fraimovich
4e4a3e13ab Fix #1824: allow only user API key to be used with query refresh API. 2017-11-01 15:05:55 +02:00
Arik Fraimovich
095d07bcb8 Disable fork button for those can't fork 2017-11-01 14:56:11 +02:00
Arik Fraimovich
71a235c79b Merge pull request #2069 from getredash/bugfixes
Fix #1979: API key of one query could be used to get results of another one
2017-11-01 14:47:23 +02:00
Arik Fraimovich
2bc3885977 Fix: require full access to the data source to fork a query.
Ref #1825.
2017-11-01 14:46:29 +02:00
Arik Fraimovich
97217f56c1 Remove unused variables 2017-11-01 13:52:41 +02:00
Arik Fraimovich
ba36f7395d Fix #1979: API key of one query could be used to get results of another one 2017-11-01 13:51:09 +02:00
Arik Fraimovich
ea7ca9e632 Merge pull request #2067 from getredash/params_refresh
Use query result for drop down values implementation updates
2017-11-01 13:50:49 +02:00
Katsuhiko YOSHIDA
5e5fc736bf Copy parameters value when forking a query 2017-11-01 06:27:03 +09:00
Arik Fraimovich
f38e76ad10 Merge pull request #2061 from kyoshidajp/delete_groupid_from_user_when_deleting_group
Delete the group id from user when deleting a group
2017-10-31 22:45:44 +02:00
Arik Fraimovich
80a6f357e3 Merge pull request #2063 from deecay/box-sortx
Fix: Boxplot xaxis sort issue
2017-10-31 22:39:26 +02:00
Arik Fraimovich
bd91288d1a Save only the query id instead of query id and name 2017-10-31 12:40:02 +02:00
Arik Fraimovich
38389a28ed Update eslint config to use longer lines 2017-10-31 12:06:52 +02:00
Arik Fraimovich
9ef9f29213 Query based parameter changes:
- Use $onChanges instead of $watch (fixes an issue where the query
results was constantly reloading).
- Choose the first value when first loading the options.
2017-10-31 12:05:55 +02:00
Arik Fraimovich
a3c2082b7f Fix: move boto3 import to the correct location 2017-10-30 14:53:22 +02:00
deecay
bc5516e941 Fix: Boxplot xaxis sort issue 2017-10-30 17:08:51 +09:00
Katsuhiko YOSHIDA
65ac8c715e Add user info to db.session 2017-10-29 22:30:53 +09:00
Arik Fraimovich
9874361466 Merge pull request #2060 from kyoshidajp/fix_export_excel
Fix error when exporting list data as Excel file
2017-10-29 15:10:34 +02:00
Arik Fraimovich
b28c8fa227 Merge pull request #2045 from myouju/master
Added 'Use Glue Data Catalog' options in Athena
2017-10-29 14:43:25 +02:00
Katsuhiko YOSHIDA
048bd53eac Delete the group id from user when deleting a group 2017-10-29 11:28:15 +09:00
Katsuhiko YOSHIDA
95c707d028 Fix error when exporting list data as Excel file 2017-10-28 23:16:02 +09:00
Arik Fraimovich
41ec4c857b Update CHANGELOG 2017-10-26 22:17:58 +03:00
Arik Fraimovich
e62acb1d99 Merge pull request #2056 from getredash/boilerplate
Reduce boilerplate in frontend code
2017-10-26 12:06:43 +03:00
Arik Fraimovich
a9dc00aaa6 Remove last relative imports 2017-10-26 10:54:28 +03:00
Arik Fraimovich
38c6152aa0 Move init code into app/config/index.js from app/index.js 2017-10-26 10:00:59 +03:00
Arik Fraimovich
fb723328d4 Add app/lib folder for general client code (instead of app/utils) 2017-10-26 10:00:36 +03:00
Arik Fraimovich
047475562d Support for non relative path imports in client code:
So this:

import { Paginator } from "../../lib";

Becomes:

import { Paginator } from "@/lib";

Makes code cleaner and more portable.
2017-10-26 09:58:24 +03:00
Arik Fraimovich
acd33ec852 Auto register Angular components, pages, etc 2017-10-25 23:34:51 +03:00
Arik Fraimovich
340a23e71c Merge pull request #2054 from jezdez/docker-entrypoint-wildcard
Allow running any command inside the container via the docker entrypoint script.
2017-10-25 22:39:55 +03:00
Jannis Leidel
3db1b7f265 Allow running any command inside the container via the docker entrypoint script.
This allows running generic things like opening a bash shell:

docker-compose run server bash
2017-10-25 20:43:32 +02:00
Arik Fraimovich
845357fa02 Add logging to route registration. 2017-10-25 17:53:28 +03:00
Arik Fraimovich
f75e31fa8e Merge pull request #2042 from isomura/modSetupShMkdir
Make /opt/redash directory if it's not exist.
2017-10-23 10:57:11 +03:00
Arik Fraimovich
38be723179 Merge branch 'master' into modSetupShMkdir 2017-10-23 10:56:50 +03:00
Arik Fraimovich
18bf44453d Update bootstrap script to use v2.0.1. 2017-10-22 15:13:30 +03:00
Arik Fraimovich
374f11252f Add v2.0.1. 2017-10-22 15:03:17 +03:00
Arik Fraimovich
2d3566abce Merge pull request #2046 from sylvainv/patch-1
Make use of REDASH_BASE_PATH variable in setup script
2017-10-22 14:47:58 +03:00
Arik Fraimovich
17d6bfff63 Merge pull request #2012 from yershalom/create_prometheus_ds
Added / on api path to prevent wrong url param
2017-10-22 09:55:41 +03:00
Arik Fraimovich
73540175d8 Merge pull request #2021 from hhamalai/configurable_invitation_token_age
Make invitation token max age configurable
2017-10-22 09:55:11 +03:00
Sylvain
8c693efb3e Add missing $REDASH_BASE_PATH usage 2017-10-19 01:39:08 -05:00
Sylvain
51392d0398 Add missing $REDASH_BASE_PATH usage 2017-10-19 01:37:11 -05:00
Sylvain
78888c2082 Make use of REDASH_BASE_PATH variable 2017-10-19 01:35:14 -05:00
Arik Fraimovich
bc6bd1b316 Merge pull request #2044 from getredash/redshift
Redshift: add support for the new ACM root CA.
2017-10-18 15:59:05 +03:00
Arik Fraimovich
4060344a72 Merge pull request #2038 from atharvai/feature/redshift-spectrum
Add support for AWS Redshift Spectrum (external) tables
2017-10-18 15:56:14 +03:00
yukimaeno
6522325060 fixed private method 2017-10-18 21:51:46 +09:00
yukimaeno
ae6564e912 Added 'Use Glue Data Catalog' options in Athena 2017-10-18 21:41:13 +09:00
Arik Fraimovich
2af70a6c2d Redshift: add support for the new ACM root CA. 2017-10-18 14:58:50 +03:00
Arik Fraimovich
a3a1dcf4ba Merge pull request #2040 from cyriac/patch-2
Show query editor's Archive/Publish Query drop-down only on saved queries
2017-10-18 11:20:31 +03:00
isomura
eb979ef130 Make /opt/redash directory if it's not exist. 2017-10-18 09:54:56 +09:00
Cyriac Thomas
7f7fdbba54 show query builder Archive/Publish Query dropdown only on saved queries 2017-10-17 20:28:20 +05:30
Arik Fraimovich
fa213d72a7 Merge pull request #2039 from getredash/filters
Improve filters UI
2017-10-17 17:02:52 +03:00
Arik Fraimovich
d2bf935edb Improve filters UI (labels for */-, label for filter, show values when selected) 2017-10-17 16:35:46 +03:00
Arik Fraimovich
c4349f5c64 Merge pull request #2037 from getredash/patches
Add: option to set allowDiskUse in MongoDB queries
2017-10-16 10:19:55 +03:00
Arik Fraimovich
b5a6f4a166 Merge pull request #2028 from kyoshidajp/autofocus_in_1st_input_item
Set auto focus in first input items
2017-10-16 10:08:19 +03:00
Arik Fraimovich
79807dfa14 Typo fix. 2017-10-16 10:07:25 +03:00
Arik Fraimovich
0b0ec90987 Update gunicorn to latest version 2017-10-16 09:19:13 +03:00
Arik Fraimovich
a9fc220ec8 Merge pull request #2034 from getredash/patches
Add: disabled status to Organization
2017-10-16 09:18:55 +03:00
Arik Fraimovich
ee9bbbaa7c Merge pull request #2024 from cyriac/patch-1
Fixed stage label typo error on sankey and sunburst-sequence editors
2017-10-15 22:51:57 +03:00
Arik Fraimovich
12cc4e5ff9 Add: option to set allowDiskUse in MongoDB queries. 2017-10-15 15:54:04 +03:00
Arik Fraimovich
b5b5643090 Add: disabled status to Organization 2017-10-15 15:51:41 +03:00
Arik Fraimovich
6718081a49 Merge pull request #2033 from getredash/patches
Add: option to disable SQLAlchemy connection pool
2017-10-15 15:51:12 +03:00
Arik Fraimovich
138087861c Add missing import 2017-10-15 15:39:41 +03:00
Arik Fraimovich
9a88cf1743 Merge branch 'patches' of github.com:getredash/redash into patches 2017-10-15 15:37:51 +03:00
Arik Fraimovich
2ca93599ef Merge pull request #2032 from getredash/patches
Add: option to set a time limit on adhoc queries
2017-10-15 15:37:22 +03:00
Arik Fraimovich
ef85a06d60 Fix import. 2017-10-15 15:16:31 +03:00
Arik Fraimovich
f7ffc75ba4 Add: option to disable SQLA connection pool. 2017-10-15 15:09:18 +03:00
Arik Fraimovich
f28eda4174 Merge pull request #2031 from getredash/patches
Add: option to disable sending an invite to a new user
2017-10-15 15:07:17 +03:00
Arik Fraimovich
c5458af1a0 Add: option to set a time limit on adhoc queries 2017-10-15 15:02:34 +03:00
Arik Fraimovich
c28ced14c6 Merge pull request #2030 from getredash/patches
Change: make log format configurable.
2017-10-15 15:00:27 +03:00
Arik Fraimovich
1110e17c4a Add: option to disable sending an invite to a new user 2017-10-15 14:56:26 +03:00
Arik Fraimovich
3b9c31a056 Change: make log format configurable. 2017-10-15 14:52:12 +03:00
Arik Fraimovich
38b655ce3a Merge pull request #2029 from getredash/patches
Change: sort series by name.
2017-10-15 14:36:41 +03:00
Arik Fraimovich
0ec9b73eb2 Change: sort series by name. 2017-10-15 14:33:55 +03:00
Katsuhiko YOSHIDA
b67369daa4 Set auto focus in first input items 2017-10-15 00:02:14 +09:00
Cyriac Thomas
cbc7eee592 fixed stage label typo on sankey and sunburst-sequence editors 2017-10-13 14:44:52 +05:30
Atharva Inamdar
d512cef5af Add support for AWS Redshift Spectrum (external) tables 2017-10-12 14:46:07 +01:00
Atharva Inamdar
c6d1fc103c Merge pull request #1 from getredash/master
merge base master with this fork
2017-10-12 14:43:54 +01:00
Harri Hämäläinen
bf5b31b252 Make invitation token max age configurable 2017-10-12 09:04:45 +03:00
Arik Fraimovich
0c404fa602 Merge pull request #1906 from kitsuyui/add-query-runner-azure-sql-data-warehouse
Query Runner for Azure SQL Data Warehouse
2017-10-11 10:27:02 +03:00
Arik Fraimovich
0ebb6ada3c Merge pull request #2017 from yutannihilation/fix-docker-compose
Fix docker-compose.production.yml
2017-10-11 10:01:25 +03:00
Hiroaki Yutani
d2e519cc3b fix docker-compose.production.yml 2017-10-11 09:53:04 +09:00
Shalom Yerushalmy
9b38f1e81c Added / on api path to prevent wrong url param 2017-10-10 16:28:11 +03:00
Arik Fraimovich
f03c173c57 Merge pull request #2003 from yershalom/create_prometheus_ds
Create prometheus ds
2017-10-10 11:43:59 +03:00
Arik Fraimovich
f89842801f Make URL required. 2017-10-10 11:41:34 +03:00
Arik Fraimovich
56d4ad74a8 Change get_schema to call requests directly. 2017-10-10 11:40:33 +03:00
Arik Fraimovich
334e95afa0 Merge pull request #2004 from modomoto/make_test_run_configurable
Allow setting test file with docker test run
2017-10-09 17:31:25 +03:00
Arik Fraimovich
0443d84848 Merge pull request #2008 from getredash/patches
Change: use outdated queries count stored already in Redis.
2017-10-09 16:43:59 +03:00
Arik Fraimovich
d38f251688 Change: use outdated queries count stored already in Redis. 2017-10-09 16:28:34 +03:00
Arik Fraimovich
890243eb20 Merge pull request #2007 from getredash/patches
Show links based on permissions the user have.
2017-10-09 16:23:34 +03:00
Arik Fraimovich
9fed3266e6 Show links based on permissions the user have. 2017-10-09 16:21:23 +03:00
Shalom Yerushalmy
8fb665be08 Some pep8 styling 2017-10-09 16:19:29 +03:00
Shalom Yerushalmy
c19253648e Changed prometheus name 2017-10-09 16:18:04 +03:00
Mehmet Emin INAC
b8d2df7567 Allow setting nosetests options via environment variable
By setting TEST_ARGS environment variable with -e option of docker-compose
we can set nosetests options to run the tests as we want, like so;

`docker-compose run --rm -e TEST_ARGS="--with-coverage tests/handlers/test_dashboards.py" server tests`
2017-10-09 15:12:04 +02:00
Arik Fraimovich
4603152930 Merge pull request #2006 from getredash/patches
Fix: support UTF8 in MySQL schema
2017-10-09 16:10:41 +03:00
Shalom Yerushalmy
e33e90a69d Remove stftime cause redash already handles this 2017-10-09 16:10:22 +03:00
Arik Fraimovich
f5dcb5d58d Merge pull request #2005 from getredash/patches
Fix: TreasureData queries were failing when returning 0 rows.
2017-10-09 16:09:45 +03:00
Arik Fraimovich
f2f6abe775 Fix: support UTF8 in MySQL schema 2017-10-09 16:09:38 +03:00
Arik Fraimovich
c33189a355 Fix: TreasureData queries were failing when returning 0 rows. 2017-10-09 16:05:58 +03:00
Shalom Yerushalmy
781d997e76 Added redash types 2017-10-09 16:05:11 +03:00
Shalom Yerushalmy
35e02d8043 Changed the timestamp to float 2017-10-09 16:03:35 +03:00
Arik Fraimovich
720af7dabf Update .gitignore 2017-10-09 13:45:58 +03:00
Shalom Yerushalmy
487a8c798c Added back the response.raise_for_status() line 2017-10-09 13:43:43 +03:00
Shalom Yerushalmy
0f580f4540 Changed file due to Arik's request 2017-10-09 13:37:23 +03:00
Arik Fraimovich
cb21024e5c Merge pull request #1981 from yershalom/upgrade_cassandra_version
Upgrade cassandra version
2017-10-09 11:39:03 +03:00
Shalom Yerushalmy
df7b970ff7 Fixed line from 123 char to 120 char due to code climate fail 2017-10-09 11:28:26 +03:00
Shalom Yerushalmy
ff4edb4fbd Added new Promethues data source 2017-10-09 11:20:30 +03:00
Arik Fraimovich
131c9ef036 Merge pull request #1976 from muddydixon/feature/docker-compose-restart-always
users using docker-compose require restart always
2017-10-09 09:36:19 +03:00
Arik Fraimovich
a3071a3ba1 Restart only postgres/redis in dev setup. 2017-10-09 09:36:09 +03:00
Arik Fraimovich
8d5ce85954 Merge pull request #1993 from deecay/box-color
Fix: Setting series color for boxplot
2017-10-09 09:28:09 +03:00
Arik Fraimovich
9d3ae2c34a Merge pull request #1998 from modomoto/fix_revoke_permissons_bug
[FIX] Revoke permission should respect to given grantee and access type.
2017-10-09 09:27:10 +03:00
Mehmet Emin INAC
6d2337b332 Revoke permission should respect to given grantee and access type.
The issue is, if you try to revoke the permission of a user from an
object, all the permissions on this object get removed. The fix is
assigning filtered query object to it's own reference.

According to SQLAlchemy documentation, `filter` method applies to
the **copy** of the query object which means calling filter doesn't
affect the object receiving filter call. For more information;
http://docs.sqlalchemy.org/en/latest/orm/query.html#sqlalchemy.orm.query.Query.filter
2017-10-06 12:52:35 +02:00
Arik Fraimovich
1ef2238d65 Merge pull request #1995 from cclauss/modernize-python2-code
Modernize Python 2 code to get ready for Python 3
2017-10-04 22:29:32 +03:00
muddydixon
521d05279b fixed according to https://github.com/getredash/redash/pull/1976#issuecomment-333370285 2017-10-04 10:01:10 +09:00
cclauss
01e85f218a Modernize Python 2 code to get ready for Python 3 2017-10-04 02:06:53 +02:00
Arik Fraimovich
8af028bc90 Merge pull request #1994 from kravets-levko/fix/eslint-error
Fixed eslint "Cannot read property 'length' of undefined" error
2017-10-03 22:49:43 +03:00
Levko Kravets
85da5fced1 Fixed eslint "Cannot read property 'length' of undefined" error 2017-10-03 21:15:10 +03:00
deecay
038d3b1004 Fix: Setting series color for boxplot 2017-10-03 10:25:07 +09:00
Arik Fraimovich
6cf2b94a10 Merge pull request #1989 from getredash/patches
Add option to set the flask-limiter storage engine
2017-10-02 17:26:27 +03:00
Arik Fraimovich
c930c44e3a Add option to set the flask-limiter storage engine 2017-10-02 17:25:54 +03:00
Arik Fraimovich
0753332ef8 Merge pull request #1988 from getredash/patches
Fix: don't crash query editor when there are unclosed curly brackets.
2017-10-02 17:01:22 +03:00
Arik Fraimovich
ed9e409e17 Fix: don't crash query editor when there are unclosed curly brackets. 2017-10-02 16:58:27 +03:00
Arik Fraimovich
c40fffa107 Merge pull request #1986 from getredash/patches
Fix: error value in charts wasn't displayed if it was 0.
2017-10-02 16:43:27 +03:00
Arik Fraimovich
d597665a86 Fix: error value in charts wasn't displayed if it was 0. 2017-10-02 16:43:06 +03:00
Arik Fraimovich
b0bec26138 Merge pull request #1975 from rohithmenon/bugfix/query_based_param
Bugfix/query based param
2017-10-01 14:32:48 +03:00
Arik Fraimovich
0d44466967 Merge pull request #1984 from getredash/patches
Cohort visualization: make it friendlier to use.
2017-10-01 14:26:47 +03:00
Arik Fraimovich
f4cb62782a Merge pull request #1983 from getredash/patches
Fix: Queries#all_queries was sometimes returning wrong number of queries
2017-10-01 14:26:20 +03:00
Arik Fraimovich
3cadd6731c Fix: tests entering endless loop, due to bad input. 2017-10-01 14:26:04 +03:00
Arik Fraimovich
fc18b84f69 Cohort visualization: make it friendlier to use.
Now it can handle gaps in data, so it's easier to generate the data needed.
2017-10-01 14:24:10 +03:00
Arik Fraimovich
f7fc679427 Merge pull request #1965 from alexmuller/firefox-textarea-keydown-prevent-enter
Prevent line breaks in EditInPlace description when using Firefox
2017-10-01 14:23:29 +03:00
Arik Fraimovich
e674b715ef Merge pull request #1966 from alexmuller/different-markdown-library
Use a different markdown library
2017-10-01 14:22:23 +03:00
Arik Fraimovich
029f6335ed Add missing import. 2017-10-01 14:19:26 +03:00
Shalom Yerushalmy
fb4153add7 Upgarde cassasndra-driver version to 3.11.0 2017-09-28 13:28:21 +03:00
Arik Fraimovich
ada8a1255b Fix: Queries#all_queries was sometimes returning wrong number of queries. 2017-09-27 18:17:57 +03:00
Arik Fraimovich
505f338da9 Merge pull request #1978 from getredash/patches
Fix #1950: record_event fails for api events
2017-09-27 18:08:57 +03:00
Arik Fraimovich
18d9b2eec9 Fix #1950: record_event fails for api events 2017-09-27 18:04:21 +03:00
muddydixon
41a03352b9 users using docker-compose require restart always 2017-09-27 17:26:39 +09:00
Rohith Menon
50f817e265 Merged with upstream 2017-09-26 23:18:50 -07:00
Rohith Menon
04ddb289ee Merged with upstream 2017-09-26 23:13:02 -07:00
Rohith Menon
0152250e14 Bugfix: column.type not set by many data sources [sqlite, postgres etc] 2017-09-26 23:07:18 -07:00
Alex Muller
f574cdd179 Use a different markdown library
`marked` has some security vulnerabilities which have been unresolved
for a while. `markdown` seems to be better supported.
2017-09-22 19:08:07 +01:00
Alex Muller
458f213ea7 Update npm-shrinkwrap
Not sure why this hadn't been updated previously.
2017-09-22 18:42:46 +01:00
Alex Muller
f2caae6eb1 Use event.preventDefault() on EditInPlace textarea
Before this change, pressing enter in Firefox 55 would insert a line
break into the description field and then save it.

This change prevents the line break from being inserted before saving.

There's no change to Chrome's behaviour from this change.
2017-09-22 17:54:02 +01:00
Alex Muller
c01cd89de9 Remove magic numbers from EditInPlace()
This makes it a lot easier to read and figure out what's going on.
2017-09-22 17:51:30 +01:00
Alex Muller
5ea3ed7308 Update redirected link in README 2017-09-22 17:51:10 +01:00
Arik Fraimovich
50eb9a86c9 Merge pull request #1961 from fan-t-endo/writer_encode_errors
UnicodeWriter errors code to environment
2017-09-21 21:39:59 +03:00
Shalom Yerushalmy
12cbfc5d12 Added timeout to cassandra 2017-09-18 12:06:35 +03:00
kitsuy
ba7ed5c6f0 Renaming SQL Server to SQL Server ODBC 2017-09-15 19:50:39 +09:00
kitsuy
4fbfa682fe Import types_map and MSSQLJSONEncoder from mssql. It is same. 2017-09-15 19:24:47 +09:00
kitsuy
fb1139a2ea Remove query encoding.
Do not have to encode query.
`execute()` take an unicode query arguments in pyodbc.
2017-09-15 19:02:20 +09:00
kitsuy
8d8ec1a5f8 Rename to Microsoft SQL Server (ODBC) 2017-09-15 18:31:33 +09:00
kitsuy
7582b3174d Add default driver in configuration_schema to specify driver more easily 2017-09-15 18:28:38 +09:00
kitsuy
154b554ecd Remove tds_version (no longer used) 2017-09-15 18:26:21 +09:00
kitsuy
316e014cfa Rename to SQLServerODBC for more precise. 2017-09-15 18:24:39 +09:00
fan-t-endo
048d8fcb5b UnicodeWriter errors code to environment 2017-09-15 17:56:27 +09:00
Arik Fraimovich
8bbb1cdfd4 Fix: wrong variable name used (dataRow instead of row)
Thanks @wu123456. 

Closes #1926.
2017-09-13 22:20:05 +03:00
Arik Fraimovich
94175b8a52 Merge pull request #1899 from queeno/add_oracle_53_support
Fix #1843: Remove deprecated cx_Oracle types
2017-09-13 18:48:14 +03:00
Simon Aquino
c350b43a5a Update oracle client version 2017-09-13 17:40:44 +02:00
Simon Aquino
b379c13e8b Update supported Oracle version 2017-09-13 17:39:04 +02:00
Simon Aquino
7d91e9d173 Fix #1843: Remove deprecated cx_Oracle types
FIXED_UNICODE, LONG_NCHAR, LONG_UNICODE and UNICODE have been removed
from cx_Oracle version 5.3 and should be removed from the TYPES_MAP.
2017-09-13 17:31:40 +02:00
Arik Fraimovich
1b15ea8af9 Merge pull request #1727 from crowdworks/salesforce-error-message
improve Salesforce error message
2017-09-13 17:39:19 +03:00
Arik Fraimovich
e76efc9cdf Merge pull request #1896 from StantonVentures/textbox_editing_fix
Textbox editing fix
2017-09-13 16:03:22 +03:00
Arik Fraimovich
0a311bf63f Merge pull request #1873 from deecay/fix-custom-js
Custom JS code chart improvements
2017-09-13 15:46:43 +03:00
Arik Fraimovich
5069edb9b1 Merge pull request #1876 from TylerBrock/ssl-postgres
Add SSL configuration option for PostgreSQL
2017-09-13 15:44:07 +03:00
Arik Fraimovich
90162b6331 Merge pull request #1920 from deecay/counter-format-string
Counter value string formatting
2017-09-13 15:39:52 +03:00
Arik Fraimovich
398812a14f Merge pull request #1928 from rohithmenon/feature/query_based_parameter
Feature/query based parameter
2017-09-13 15:35:50 +03:00
Arik Fraimovich
2e44872b49 Merge pull request #1955 from getredash/fix_mysql
MySQL: multiple queries support & connection timeout
2017-09-13 14:48:44 +03:00
Arik Fraimovich
e02fdb3e37 MySQL: add support for multiple queries (returning results only of the last one) 2017-09-13 14:38:56 +03:00
Arik Fraimovich
234edd339c MySQL: add connection timeout for bad hosts 2017-09-13 14:38:28 +03:00
Arik Fraimovich
e5cbdf3036 Merge pull request #1946 from Posnet/select-all
Add ability to easily select all for multi-filter
2017-09-13 14:29:15 +03:00
Arik Fraimovich
9b85890204 Merge pull request #1954 from labradorcouk/master
Upgraded dql version to 0.5.24
2017-09-13 14:26:53 +03:00
Antonio Terreno
6295e88d43 Upgraded dql version to 0.5.24 - this allows to query tables in dynamo which have keys with dashes in the name 2017-09-13 09:12:44 +01:00
Arik Fraimovich
7796a57d43 Merge pull request #1930 from mfouilleul/master
Cassandra: get_schema support for both C* 2.x and 3.x, support for SortedSet type serialization.
2017-09-12 15:59:36 +03:00
Rohith Menon
df7fd13bfd Hovertext length (#3)
* Namelength for hoverlabel to avoid truncation

* Update npm-shinkwrap.json
2017-09-06 21:08:18 -07:00
Rohith Menon
6a5a843478 Merge branch 'master' of https://github.com/getredash/redash 2017-09-05 10:35:40 -07:00
Alec Posney
7d4fb280ba Add ability to easily select all for multi-filter
The multi filter option is useful but lacking in an easy easy way to
select all values. I have added in a psudo option '*' that when selected
automatically fills out the mutli-select with all possible filters.

I have also added in a second psudo option '-' which becomes available
_if_ the multi-filter has all possible values selected.
This makes it easy to clear the multi-filter.
2017-09-05 13:31:04 +10:00
Arik Fraimovich
2a22b98c77 Merge pull request #1944 from getredash/fix_permissions
Fix: collaborators couldn't edit visualizations or schedule
2017-09-03 15:00:27 +03:00
Arik Fraimovich
6b56e4a3e3 Allow collaborators to update query schedule. 2017-09-03 14:31:42 +03:00
Arik Fraimovich
47fc6612bf Allow collaborators to create, delete and edit visualizations. 2017-09-03 14:28:34 +03:00
Rohith Menon
f3e5c22c07 Merge/query based parameter (#2)
* Feature: Query based parameter (drop-down)

* Restrict to string column for query parameter

* Fix lint errors

* Fix html in paramters.html

* Addressed comments from @arikfr
2017-08-23 20:48:02 -07:00
Maxime Fouilleul
b42d2c5784 Fix codeclimate notices (trailing space) 2017-08-17 18:19:19 +02:00
Maxime Fouilleul
478a86a892 Fix codeclimate notices (SQL) 2017-08-17 18:17:53 +02:00
Maxime Fouilleul
9e0205d148 Improve and fix cassandra query runner 2017-08-17 18:10:16 +02:00
Rohith Menon
59b7961bcd Addressed comments from @arikfr 2017-08-16 16:04:00 -07:00
Arik Fraimovich
5b54a777d9 Merge pull request #1863 from 44px/ng-annotate-deprecation
Replace deprecated ng-annotate with babel plugin
2017-08-16 16:21:23 +03:00
Arik Fraimovich
3af9b333a8 Merge pull request #1898 from StantonVentures/security_lib_updates_7_27_2017
update libraries
2017-08-16 16:19:57 +03:00
Arik Fraimovich
dcaecdbe16 Merge pull request #1921 from deecay/error-bar-color
Fix: error bar color in sync with series color
2017-08-16 16:19:04 +03:00
Arik Fraimovich
3aa7d86699 Update bootstrap.sh 2017-08-16 16:16:00 +03:00
Rohith Menon
feab2a7e7b Fix html in paramters.html 2017-08-15 13:53:55 -07:00
Rohith Menon
d18220c1af Feature/query based parameter (#1)
* Feature: Query based parameter (drop-down)

* Restrict to string column for query parameter

* Fix lint errors
2017-08-14 21:47:48 -07:00
Rohith Menon
8074a91b29 Fix lint errors 2017-08-14 09:20:58 -07:00
Rohith Menon
72560d985f Restrict to string column for query parameter 2017-08-14 09:15:28 -07:00
Rohith Menon
ff2c8524de Feature: Query based parameter (drop-down) 2017-08-14 00:38:41 -07:00
Alison
1bdea11fe3 updates based on PR comments 2017-08-11 21:17:30 -05:00
Arik Fraimovich
a7bed64707 Merge pull request #1836 from amarjayr/master
LDAP (Active Directory) implementation
2017-08-09 20:46:17 +03:00
Arik Fraimovich
dc969fe0b5 Bump version. 2017-08-09 20:45:26 +03:00
Amar Ramachandran
588c868060 Make ldap3 requirement optional 2017-08-09 10:32:44 -07:00
deecay
e739f90405 Fix: error bar color in sync with series color 2017-08-08 18:27:42 +09:00
deecay
a07135c638 Move counter visualiation formatting controls to tab 2017-08-08 17:45:37 +09:00
kitsuy
974f69aecf Query Runner for Azure SQL Data Warehouse
- This is almost copied from mssql.py.
- Microsoft's driver installation is here: https://www.microsoft.com/en-us/sql-server/developer-get-started/node/ubuntu/
2017-08-03 13:22:45 +09:00
deecay
1a8078ab03 Fix: Custom code keeps appending trace per refresh 2017-07-31 19:22:25 +09:00
Alison
1bc8d586c3 update libraries
Based on pyup auto-PR httplib2 and cryptography needed updating which
necessitated updating pyOpenSSL as well.
2017-07-27 21:15:39 -05:00
Alison
a795f1463b Fixes dashboard textbox editing
Combines mozilla/redash PR’s 86 and 95.

There was a bug that saved textbox content on a dashboard when you
tried to close without saving. This fixes it.
2017-07-26 23:15:22 -05:00
Alison
aae77a8b25 Merge remote-tracking branch 'getredash/master' 2017-07-25 15:19:24 -05:00
deecay
c278209883 Counter visualiation formatting 2017-07-25 19:26:52 +09:00
Tyler Brock
6d8880c10d Add SSL configuration option for PostgreSQL 2017-07-19 11:14:40 -07:00
deecay
aacc4b7b46 Fix: Custom code keeps appending trace per refresh 2017-07-11 17:44:05 +09:00
deecay
605a70d554 Add toggle for automatically updating graph 2017-07-11 17:31:16 +09:00
deecay
73466dc0e0 Make custom js textarea resizable 2017-07-11 15:43:09 +09:00
deecay
3fd90c6289 Fix: Custom code didn't load into editor 2017-07-11 10:15:47 +09:00
Alexander Shepelin
53f0716aca Replace deprecated ng-annotate with babel plugin 2017-07-02 14:22:36 +03:00
Amar Ramachandran
b9e08897ac Move ldap auth logic to function 2017-06-29 11:33:57 -07:00
Amar Ramachandran
300421792c Add log error when LDAP connection fails
Integrate ldap login in login template
2017-06-28 13:32:24 -07:00
Amar Ramachandran
85f729260b Clean up file 2017-06-20 15:33:32 -07:00
Amar Ramachandran
8bf2c15db8 Add ldap auth logic 2017-06-20 15:33:32 -07:00
Amar Ramachandran
9ea4784f87 Add ldap3 requirement 2017-06-20 15:33:32 -07:00
Amar Ramachandran
8be9613640 Add ldap env. config settings 2017-06-20 15:33:32 -07:00
Amar Ramachandran
b611c98112 Add ldap blueprint 2017-06-20 15:33:32 -07:00
Akira Yumiyama
f852f935c5 improve Salesforce error message 2017-04-18 23:37:05 +09:00
229 changed files with 15915 additions and 4779 deletions

8
.gitignore vendored
View File

@@ -9,14 +9,8 @@ celerybeat-schedule*
\#*#
*~
_build
# Vagrant related
.vagrant
Berksfile.lock
redash/dump.rdb
.vscode
.env
.ruby-version
venv
dump.rdb

View File

@@ -1,5 +1,87 @@
# Change Log
## v3.0.0 - UNRELEASED
### Added
- Query Result data source (run queries on query results).
- Athena: option to load schema from Glue catalog. @myouju
- Allow running any command inside the container via the Docker entrypoint script. @jezdez
- Make invitation token max age configurable. @hhamalai
- Redshift: add support for the new ACM root CA.
- Redshift: support for Spectrum (external) tables. @atharvai
- MongoDB: option to set allowDiskUse in queries.
- Option to disable SQLAlchemy connection pool.
- Option to set a time limit on adhoc queries.
- Option to disable sending an invite to a new user.
- Azure SQL Data Warehouse query runner. @kitsuyui
- Prometheus query runner. @yershalom
- Option to set the Flask-Limiter storage engine.
- Option to set UnicodeWriter's error handling method. @fan-t-endo
- PostgreSQL: SSL configuration option. @TylerBrock
- Counter visualization: additional formatting options. @deecay
- Query based drop down parameter. @rohithmenon
- MySQL: multiple queries support & connection timeout.
- Ability to select all in multi-filter. @Posnet
- LDAP (Active Directory) support. @amarjayr
### Changed
- Copy parameters when forking a query. @kyoshidajp
- Prevent using Query API Key with refresh API (previously it was just failing).
- Reduce boilerplate in frontend code.
- Set auto focus in first input items. @kyoshidajp
- Update gunicorn to latest version.
- Make log format configurable.
- Sort series by name.
- Allow setting test file with Docker test run. @meinac
- Use outdated queries count stored already in Redis.
- Show links based on permissions the user have.
- Cassandra: update driver version. @yershalom
- Docker-Compose: update configuration to always restart services. @muddydixon
- Modernize Python 2 code to get ready for Python 3. @cclauss
- Cohort visualization: make it friendlier to use by better handle gaps in data, so it's easier to generate the data needed.
- Use a different markdown library. @alexmuller
- Salesforce: improve error messages we receive from the API. @akiray03
- Custom JS code visualization improvements. @deecay
- DQL: Update version to 0.5.24. @aterreno
- Cassandra: get_schema support for both C* 2.x and 3.x, support for SortedSet type serialization. (@mfouilleul)
- Replace deprecated ng-annotate with babel plugin. @44px
- Update Python dependencies to recent versions. @alison985
- Bootstrap script: create /opt/redash directory only if it doesn't exist. @isomura
- Bootstrap script: make use of REDASH_BASE_PATH variable in setup script. @sylvain
### Fixed
- Require full data source access to fork a query.
- API key of one query could be used to get results of another one.
- Delete group id from user object when deleting the group. @kyoshidajp
- Sorting of X axis wasn't working for Box plot type visualizations. @deecay
- Exporting query results as excel was failing when one of the columns had array data. @kyoshidajp
- Show query editor's Archive/Publish Query drop-down only on saved queries. @cyriac
- Move misplaced configuration in docker-compose.production.yml. @yutannihilation
- MySQL: support UTF8 schema.
- TreasureData queries were failing when returning 0 rows.
- Use series color for Boxplot. @deecay
- Revoke permission should respect to given grantee and access type. @meinac
- Fixed eslint "Cannot read property 'length' of undefined" error. @kravets-levko
- Don't crash query editor when there are unclosed curly brackets.
- Error value in charts wasn't displayed if it was 0.
- Prevent line breaks in EditInPlace description when using Firefox. @alexmuller
- Queries#all_queries was sometimes returning wrong number of queries.
- record_event fails for API events.
- Cancel button on tasks admin page was broken.
- Remove deprecated cx_Oracle types. @queeno
- Textbox widgets were updating their value even when editor was cancelled. @alison985
- Collaborators couldn't edit visualizations or schedule.
- Use series color for error bar. @deecay
- Upgrade script was using the wrong restart command on new AMIs.
## v2.0.1 - 2017-10-22
This is a patch release, that adds support for Redshift ACM certificates (see #2044 for details).
## v2.0.0 - 2017-08-08
### Added

View File

@@ -4,6 +4,7 @@ FULL_VERSION=$(VERSION)+b$(CIRCLE_BUILD_NUM)
BASE_VERSION=$(shell python ./manage.py version | cut -d + -f 1)
# 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
TEST_ARGS?=--with-coverage --cover-package=redash tests/
deps:
if [ -d "./client/app" ]; then npm install; fi
@@ -17,4 +18,4 @@ upload:
python bin/release_manager.py $(CIRCLE_SHA1) $(BASE_VERSION) $(FILENAME)
test:
nosetests --with-coverage --cover-package=redash tests/
nosetests $(TEST_ARGS)

View File

@@ -43,7 +43,7 @@ You can try out the demo instance: http://demo.redash.io/ (login with any Google
## Reporting Bugs and Contributing Code
* Want to report a bug or request a feature? Please open [an issue](https://github.com/getredash/redash/issues/new).
* Want to help us build **_Redash_**? Fork the project, edit in a [dev environment](https://redash.io/help-onpremise/setup/setting-up-development-environment-using-vagrant.html), and make a pull request. We need all the help we can get!
* Want to help us build **_Redash_**? Fork the project, edit in a [dev environment](https://redash.io/help-onpremise/dev/guide.html), and make a pull request. We need all the help we can get!
## License

View File

@@ -72,7 +72,10 @@ case "$1" in
tests)
tests
;;
*)
help)
help
;;
*)
exec "$@"
;;
esac

View File

@@ -1,3 +1,4 @@
from __future__ import print_function
import os
import sys
import json
@@ -95,7 +96,7 @@ def get_changelog(commit_sha):
try:
pull_request = re.match("Merge pull request #(\d+)", subject).groups()[0]
pull_request = " #{}".format(pull_request)
except Exception, ex:
except Exception as ex:
pull_request = ""
author = subprocess.check_output(['git', 'log', '-1', '--pretty=format:"%an"', parents.split(' ')[-1]])[1:-1]
@@ -124,7 +125,7 @@ def update_release(version, build_filepath, commit_sha):
else:
release = create_release(version, commit_sha)
print "Using release id: {}".format(release['id'])
print("Using release id: {}".format(release['id']))
remove_previous_builds(release)
response = upload_asset(release, build_filepath)
@@ -135,8 +136,8 @@ def update_release(version, build_filepath, commit_sha):
if response.status_code != 200:
raise exception_from_error("Failed updating release description", response)
except Exception, ex:
print ex
except Exception as ex:
print(ex)
if __name__ == '__main__':
commit_sha = sys.argv[1]

View File

@@ -18,7 +18,7 @@ test:
- nosetests --with-xunit --xunit-file=$CIRCLE_TEST_REPORTS/junit.xml --with-coverage --cover-package=redash tests/
deployment:
github_and_docker:
branch: master
branch: [master, /release.*/]
commands:
- make pack
# Skipping uploads for now, until master is stable.

View File

@@ -1,4 +1,4 @@
{
"presets": ["es2015", "stage-2"],
"plugins": ["transform-object-assign"]
"plugins": ["angularjs-annotate", "transform-object-assign"]
}

View File

@@ -1,2 +1,3 @@
build/*.js
config/*.js
node_modules

View File

@@ -1,15 +1,30 @@
module.exports = {
root: true,
extends: 'airbnb-base',
extends: "airbnb-base",
settings: {
"import/resolver": "webpack"
},
env: {
"browser": true,
"node": true
},
rules: {
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
'no-param-reassign': 0,
'no-mixed-operators': 0,
'no-underscore-dangle': 0,
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
"prefer-destructuring": "off",
"prefer-template": "off",
"no-restricted-properties": "off",
"no-restricted-globals": "off",
"no-multi-assign": "off",
"max-len": ['error', 120, 2, {
ignoreUrls: true,
ignoreComments: false,
ignoreRegExpLiterals: true,
ignoreStrings: true,
ignoreTemplateLiterals: true,
}]
}
}
};

View File

@@ -1781,6 +1781,9 @@ fieldset[disabled] .form-control {
textarea.form-control {
height: auto;
}
textarea.v-resizable {
resize: vertical;
}
input[type="search"] {
-webkit-appearance: none;
}
@@ -8592,6 +8595,7 @@ a.thumbnail.active {
padding: 0;
white-space: nowrap;
margin: 0;
margin-bottom: 10px;
overflow: auto;
box-shadow: inset 0 -2px 0 0 #eee;
}

View File

@@ -0,0 +1,113 @@
import { contains, without, compact } from 'underscore';
import template from './alert-subscriptions.html';
function controller($scope, $q, $sce, currentUser, AlertSubscription, Destination, toastr) {
'ngInject';
$scope.newSubscription = {};
$scope.subscribers = [];
$scope.destinations = [];
$scope.currentUser = currentUser;
$q
.all([
Destination.query().$promise,
AlertSubscription.query({ alertId: $scope.alertId }).$promise,
])
.then((responses) => {
const destinations = responses[0];
const subscribers = responses[1];
const mapF = s => s.destination && s.destination.id;
const subscribedDestinations = compact(subscribers.map(mapF));
const subscribedUsers = compact(subscribers.map(s => !s.destination && s.user.id));
$scope.destinations = destinations.filter(d => !contains(subscribedDestinations, d.id));
if (!contains(subscribedUsers, currentUser.id)) {
$scope.destinations.unshift({ user: { name: currentUser.name } });
}
$scope.newSubscription.destination = $scope.destinations[0];
$scope.subscribers = subscribers;
});
$scope.destinationsDisplay = (d) => {
if (!d) {
return '';
}
let destination = d;
if (d.destination) {
destination = destination.destination;
} else if (destination.user) {
destination = {
name: `${d.user.name} (Email)`,
icon: 'fa-envelope',
type: 'user',
};
}
return $sce.trustAsHtml(`<i class="fa ${destination.icon}"></i>&nbsp;${destination.name}`);
};
$scope.saveSubscriber = () => {
const sub = new AlertSubscription({ alert_id: $scope.alertId });
if ($scope.newSubscription.destination.id) {
sub.destination_id = $scope.newSubscription.destination.id;
}
sub.$save(
() => {
toastr.success('Subscribed.');
$scope.subscribers.push(sub);
$scope.destinations = without($scope.destinations, $scope.newSubscription.destination);
if ($scope.destinations.length > 0) {
$scope.newSubscription.destination = $scope.destinations[0];
} else {
$scope.newSubscription.destination = undefined;
}
},
() => {
toastr.error('Failed saving subscription.');
},
);
};
$scope.unsubscribe = (subscriber) => {
const destination = subscriber.destination;
const user = subscriber.user;
subscriber.$delete(
() => {
toastr.success('Unsubscribed');
$scope.subscribers = without($scope.subscribers, subscriber);
if (destination) {
$scope.destinations.push(destination);
} else if (user.id === currentUser.id) {
$scope.destinations.push({ user: { name: currentUser.name } });
}
if ($scope.destinations.length === 1) {
$scope.newSubscription.destination = $scope.destinations[0];
}
},
() => {
toastr.error('Failed unsubscribing.');
},
);
};
}
export default function init(ngModule) {
ngModule.directive('alertSubscriptions', () => ({
restrict: 'E',
replace: true,
scope: {
alertId: '=',
},
template,
controller,
}));
}

View File

@@ -32,7 +32,7 @@
<li><a href="queries">Queries</a></li>
</ul>
</li>
<li>
<li ng-if="$ctrl.showAlertsLink">
<a href="alerts">Alerts</a>
</li>
</ul>
@@ -59,7 +59,7 @@
<a ng-href="users/{{$ctrl.currentUser.id}}">
<div class="row">
<div class="col-sm-2">
<img ng-src="{{$ctrl.currentUser.gravatar_url}}" size="40px" class="img-circle"/>
<img ng-src="{{$ctrl.currentUser.gravatar_url}}" size="40px" class="img-circle" />
</div>
<div class="col-sm-10">
<p><strong>{{$ctrl.currentUser.name}}</strong></p>
@@ -68,15 +68,15 @@
</a>
</li>
<li class="divider" ng-if="$ctrl.currentUser.hasPermission('super_admin')">
<li ng-if="$ctrl.currentUser.hasPermission('super_admin')"><a href="admin/status">System Status</a></li>
<li class="divider">
</li>
<li>
<a ng-click="$ctrl.logout()">Log out</a>
</li>
<li ng-if="$ctrl.currentUser.hasPermission('super_admin')"><a href="admin/status">System Status</a></li>
<li class="divider">
</li>
<li>
<a ng-click="$ctrl.logout()">Log out</a>
</li>
</ul>
</li>
</li>
</ul>
</div>
</div>
</nav>
</nav>

View File

@@ -1,7 +1,7 @@
import debug from 'debug';
import logoUrl from '@/assets/images/redash_icon_small.png';
import template from './app-header.html';
import logoUrl from '../../assets/images/redash_icon_small.png';
import './app-header.css';
const logger = debug('redash:appHeader');
@@ -11,6 +11,7 @@ function controller($rootScope, $location, $uibModal, Auth, currentUser, clientC
this.basePath = clientConfig.basePath;
this.currentUser = currentUser;
this.showQueriesMenu = currentUser.hasPermission('view_query');
this.showAlertsLink = currentUser.hasPermission('list_alerts');
this.showNewQueryMenu = currentUser.hasPermission('create_query');
this.showSettingsMenu = currentUser.hasPermission('list_users');
this.showDashboardsMenu = currentUser.hasPermission('list_dashboards');
@@ -42,7 +43,7 @@ function controller($rootScope, $location, $uibModal, Auth, currentUser, clientC
};
}
export default function (ngModule) {
export default function init(ngModule) {
ngModule.component('appHeader', {
template,
controller,

View File

@@ -27,6 +27,6 @@ function cancelQueryButton() {
};
}
export default function (ngModule) {
export default function init(ngModule) {
ngModule.directive('cancelQueryButton', cancelQueryButton);
}

View File

@@ -17,6 +17,8 @@ const AddWidgetDialog = {
this.query = {};
this.selected_query = undefined;
this.text = '';
this.existing_text = '';
this.new_text = '';
this.widgetSizes = [{
name: 'Regular',
value: 1,
@@ -95,6 +97,6 @@ const AddWidgetDialog = {
},
};
export default function (ngModule) {
export default function init(ngModule) {
ngModule.component('addWidgetDialog', AddWidgetDialog);
}

View File

@@ -4,7 +4,7 @@
</div>
<div class="modal-body">
<p>
<input type="text" class="form-control" placeholder="Dashboard Name" ng-model="$ctrl.dashboard.name">
<input type="text" class="form-control" placeholder="Dashboard Name" ng-model="$ctrl.dashboard.name" autofocus>
</p>
<p ng-if="$ctrl.dashboard.id">

View File

@@ -99,6 +99,6 @@ const EditDashboardDialog = {
},
};
export default function (ngModule) {
export default function init(ngModule) {
ngModule.component('editDashboardDialog', EditDashboardDialog);
}

View File

@@ -4,11 +4,11 @@
</div>
<div class="modal-body">
<div class="form-group">
<textarea class="form-control" ng-model="$ctrl.widget.text" rows="3"></textarea>
<textarea class="form-control" ng-model="$ctrl.widget.new_text" rows="3"></textarea>
</div>
<div ng-show="$ctrl.widget.text">
<div ng-show="$ctrl.widget.new_text">
<strong>Preview:</strong>
<p ng-bind-html="$ctrl.widget.text | markdown"></p>
<p ng-bind-html="$ctrl.widget.new_text | markdown"></p>
</div>
</div>

View File

@@ -15,13 +15,18 @@ const EditTextBoxComponent = {
this.widget = this.resolve.widget;
this.saveWidget = () => {
this.saveInProgress = true;
this.widget.$save().then(() => {
if (this.widget.new_text !== this.widget.existing_text) {
this.widget.text = this.widget.new_text;
this.widget.$save().then(() => {
this.close();
}).catch(() => {
toastr.error('Widget can not be updated');
}).finally(() => {
this.saveInProgress = false;
});
} else {
this.close();
}).catch(() => {
toastr.error('Widget can not be updated');
}).finally(() => {
this.saveInProgress = false;
});
}
};
},
};
@@ -30,6 +35,8 @@ function DashboardWidgetCtrl($location, $uibModal, $window, Events, currentUser)
this.canViewQuery = currentUser.hasPermission('view_query');
this.editTextBox = () => {
this.widget.existing_text = this.widget.text;
this.widget.new_text = this.widget.text;
$uibModal.open({
component: 'editTextBox',
resolve: {
@@ -92,7 +99,7 @@ function DashboardWidgetCtrl($location, $uibModal, $window, Events, currentUser)
}
}
export default function (ngModule) {
export default function init(ngModule) {
ngModule.component('editTextBox', EditTextBoxComponent);
ngModule.component('dashboardWidget', {
template,

View File

@@ -1,7 +1,7 @@
<form name="dataSourceForm">
<div class="form-group">
<label for="type">Type</label>
<select name="type" class="form-control" ng-options="type.type as type.name for type in types" ng-model="target.type"></select>
<select name="type" class="form-control" ng-options="type.type as type.name for type in types" ng-model="target.type" autofocus></select>
</div>
<div class="form-group">
<label for="dataSourceName">Name</label>

View File

@@ -38,7 +38,7 @@ function DynamicForm($http, toastr, $q) {
$scope.fields = orderedInputs(
configurationSchema.properties,
configurationSchema.order || []
configurationSchema.order || [],
);
return type;
@@ -137,13 +137,13 @@ function DynamicForm($http, toastr, $q) {
} else {
toastr.error('Failed saving.');
}
}
},
);
};
},
};
}
export default function (ngModule) {
export default function init(ngModule) {
ngModule.directive('dynamicForm', DynamicForm);
}

View File

@@ -64,7 +64,7 @@ function DynamicTable($sanitize) {
};
}
export default function (ngModule) {
export default function init(ngModule) {
ngModule.component('dynamicTable', {
template,
controller: DynamicTable,

View File

@@ -33,6 +33,8 @@ function EditInPlace() {
link($scope, element) {
// Let's get a reference to the input element, as we'll want to reference it.
const inputElement = $(element.children()[2]);
const keycodeEnter = 13;
const keycodeEscape = 27;
// This directive should have a set class so we can style it.
element.addClass('edit-in-place');
@@ -74,9 +76,10 @@ function EditInPlace() {
$(inputElement).keydown((e) => {
// 'return' or 'enter' key pressed
// allow 'shift' to break lines
if (e.which === 13 && !e.shiftKey) {
if (e.which === keycodeEnter && !e.shiftKey) {
e.preventDefault();
save();
} else if (e.which === 27) {
} else if (e.which === keycodeEscape) {
$scope.value = $scope.oldValue;
$scope.$apply(() => {
$(inputElement[0]).blur();
@@ -89,6 +92,6 @@ function EditInPlace() {
};
}
export default function (ngModule) {
export default function init(ngModule) {
ngModule.directive('editInPlace', EditInPlace);
}

View File

@@ -2,7 +2,7 @@ function controller(clientConfig, currentUser) {
this.showMailWarning = clientConfig.mailSettingsMissing && currentUser.isAdmin;
}
export default function (ngModule) {
export default function init(ngModule) {
ngModule.component('emailSettingsWarning', {
bindings: {
function: '<',

View File

@@ -15,6 +15,6 @@ const ErrorMessagesComponent = {
},
};
export default function (ngModule) {
export default function init(ngModule) {
ngModule.component('errorMessages', ErrorMessagesComponent);
}

View File

@@ -1,20 +1,34 @@
<div class="container bg-white p-5" ng-show="$ctrl.filters | notEmpty">
<div class="row">
<div class="col-sm-6 m-t-5" ng-repeat="filter in $ctrl.filters">
<ui-select ng-model="filter.current" ng-if="!filter.multiple" on-select="$ctrl.filterChangeListener(filter, $model)" on-remove="$ctrl.filterChangeListener(filter, $model)">
<ui-select-match placeholder="Select value for {{filter.friendlyName}}...">{{filter.friendlyName}}: {{$select.selected | filterValue:filter}}</ui-select-match>
<label>{{filter.friendlyName}}</label>
</div>
</div>
<div class="row">
<div class="col-sm-6 m-t-5" ng-repeat="filter in $ctrl.filters">
<ui-select ng-model="filter.current" ng-if="!filter.multiple" on-select="$ctrl.filterChangeListener(filter, $model)" on-remove="$ctrl.filterChangeListener(filter, $model)"
remove-selected="false">
<ui-select-match placeholder="Select value for {{filter.friendlyName}}...">{{$select.selected | filterValue:filter}}</ui-select-match>
<ui-select-choices repeat="value in filter.values | filter: $select.search">
{{value | filterValue:filter }}
</ui-select-choices>
</ui-select>
<ui-select ng-model="filter.current" multiple ng-if="filter.multiple" on-select="$ctrl.filterChangeListener(filter, $model)" on-remove="$ctrl.filterChangeListener(filter, $model)">
<ui-select-match placeholder="Select value for {{filter.friendlyName}}...">{{filter.friendlyName}}: {{$item | filterValue:filter}}</ui-select-match>
<ui-select-choices repeat="value in filter.values | filter: $select.search">
{{value | filterValue:filter }}
<ui-select ng-model="filter.current" multiple ng-if="filter.multiple" on-select="$ctrl.filterChangeListener(filter, $model)"
on-remove="$ctrl.filterChangeListener(filter, $model)" remove-selected="false">
<ui-select-match placeholder="Select value for {{filter.friendlyName}}...">{{$item | filterValue:filter}}</ui-select-match>
<ui-select-choices repeat="value in filter.values | filter: $select.search" group-by="$ctrl.itemGroup">
<span ng-if="value == '*'">
Select All
</span>
<span ng-if="value == '-'">
Clear
</span>
<span ng-if="value != '*' && value != '-'">
{{value | filterValue:filter }}
</span>
</ui-select-choices>
</ui-select>
</div>
</div>
</div>
</div>

View File

@@ -12,10 +12,18 @@ const FiltersComponent = {
this.filterChangeListener = (filter, modal) => {
this.onChange({ filter, $modal: modal });
};
this.itemGroup = (item) => {
if (item === '*' || item === '-') {
return '';
}
return 'Values';
};
},
};
export default function (ngModule) {
export default function init(ngModule) {
ngModule.component('filters', FiltersComponent);
}

View File

@@ -5,7 +5,7 @@ function controller(clientConfig, currentUser) {
this.newVersionAvailable = clientConfig.newVersionAvailable && currentUser.isAdmin;
}
export default function (ngModule) {
export default function init(ngModule) {
ngModule.component('footer', {
template,
controller,

View File

@@ -3,7 +3,7 @@
</div>
<div class="modal-body">
<form class="form">
<input type="text" ng-model="$ctrl.group.name" placeholder="Group Name" class="form-control"/>
<input type="text" ng-model="$ctrl.group.name" placeholder="Group Name" class="form-control" autofocus/>
</form>
</div>
<div class="modal-footer">

View File

@@ -34,6 +34,6 @@ const EditGroupDialogComponent = {
},
};
export default function (ngModule) {
export default function init(ngModule) {
ngModule.component('editGroupDialog', EditGroupDialogComponent);
}

View File

@@ -15,7 +15,7 @@ function controller($window, $location, toastr, currentUser) {
};
}
export default function (ngModule) {
export default function init(ngModule) {
ngModule.component('groupName', {
bindings: {
group: '<',

View File

@@ -1,21 +0,0 @@
export { default as appHeader } from './app-header';
export { default as footer } from './footer';
export { default as pageHeader } from './page-header';
export { default as tabNav } from './tab-nav';
export { default as emailSettingsWarning } from './email-settings-warning';
export { default as rdTab } from './rd-tab';
export { default as queryLink } from './query-link';
export { default as parameters } from './parameters';
export { default as permissionsEditor } from './permissions-editor';
export { default as dynamicTable } from './dynamic-table';
export { default as paginator } from './paginator';
export { default as settingsScreen } from './settings-screen';
export { default as errorMessages } from './error-messages';
export { default as editInPlace } from './edit-in-place';
export { default as dynamicForm } from './dynamic-form';
export { default as rdTimer } from './rd-timer';
export { default as rdTimeAgo } from './rd-time-ago';
export { default as overlay } from './overlay';
export { default as routeStatus } from './route-status';
export { default as filters } from './filters';
export { default as sortIcon } from './sort-icon';

View File

@@ -11,6 +11,6 @@ const Overlay = {
transclude: true,
};
export default function (ngModule) {
export default function init(ngModule) {
ngModule.component('overlay', Overlay);
}

View File

@@ -4,7 +4,7 @@ function controller() {
}
export default function (ngModule) {
export default function init(ngModule) {
ngModule.component('pageHeader', {
template,
controller,

View File

@@ -7,7 +7,7 @@ class PaginatorCtrl {
}
}
export default function (ngModule) {
export default function init(ngModule) {
ngModule.component('paginator', {
template: `
<div class="text-center">

View File

@@ -14,18 +14,32 @@
<option value="text">Text</option>
<option value="number">Number</option>
<option value="enum">Dropdown List</option>
<option value="query">Query Based Dropdown List</option>
<option value="date">Date</option>
<option value="datetime-local">Date and Time</option>
<option value="datetime-with-seconds">Date and Time (with seconds)</option>
</select>
</div>
<div class="form-group">
<label>Global</label>
<input type="checkbox" class="form-inline" ng-model="$ctrl.parameter.global">
<label>
<input type="checkbox" class="form-inline" ng-model="$ctrl.parameter.global">
Global
</label>
</div>
<div class="form-group" ng-if="$ctrl.parameter.type === 'enum'">
<label>Dropdown List Values (newline delimited)</label>
<textarea class="form-control" rows="3" ng-model="$ctrl.parameter.enumOptions"></textarea>
</div>
<div class="form-group" ng-if="$ctrl.parameter.type === 'query'">
<label>Query to load dropdown values from:</label>
<ui-select ng-model="$ctrl.parameter.queryId" reset-search-input="false">
<ui-select-match placeholder="Search a query by name">{{$select.selected.name}}</ui-select-match>
<ui-select-choices repeat="q.id as q in $ctrl.queries"
refresh="$ctrl.searchQueries($select.search)"
refresh-delay="0">
<div class="form-group" ng-bind-html="$ctrl.trustAsHtml(q.name | highlight: $select.search)"></div>
</ui-select-choices>
</ui-select>
</div>
</div>
</div>

View File

@@ -19,6 +19,9 @@
<option ng-repeat="option in extractEnumOptions(param.enumOptions)" value="{{option}}">{{option}}</option>
</select>
</span>
<span ng-switch-when="query">
<query-based-parameter param="param" query-id="param.queryId"></query-based-parameter>
</span>
<input ng-switch-default type="{{param.type}}" class="form-control" ng-model="param.ngModel">
</span>
</div>

View File

@@ -1,4 +1,6 @@
import { find } from 'underscore';
import template from './parameters.html';
import queryBasedParameterTemplate from './query-based-parameter.html';
import parameterSettingsTemplate from './parameter-settings.html';
const ParameterSettingsComponent = {
@@ -8,10 +10,96 @@ const ParameterSettingsComponent = {
close: '&',
dismiss: '&',
},
controller() {
controller($sce, Query) {
'ngInject';
this.trustAsHtml = html => $sce.trustAsHtml(html);
this.parameter = this.resolve.parameter;
if (this.parameter.queryId) {
Query.get({ id: this.parameter.queryId }, (query) => {
this.queries = [query];
});
}
this.searchQueries = (term) => {
if (!term || term.length < 3) {
return;
}
Query.search({ q: term }, (results) => {
this.queries = results;
});
};
},
};
function optionsFromQueryResult(queryResult) {
const columns = queryResult.data.columns;
const numColumns = columns.length;
let options = [];
// If there are multiple columns, check if there is a column
// named 'name' and column named 'value'. If name column is present
// in results, use name from name column. Similar for value column.
// Default: Use first string column for name and value.
if (numColumns > 0) {
let nameColumn = null;
let valueColumn = null;
columns.forEach((column) => {
const columnName = column.name.toLowerCase();
if (columnName === 'name') {
nameColumn = column.name;
}
if (columnName === 'value') {
valueColumn = column.name;
}
// Assign first string column as name and value column.
if (nameColumn === null) {
nameColumn = column.name;
}
if (valueColumn === null) {
valueColumn = column.name;
}
});
if (nameColumn !== null && valueColumn !== null) {
options = queryResult.data.rows.map((row) => {
const queryResultOption = {
name: row[nameColumn],
value: row[valueColumn],
};
return queryResultOption;
});
}
}
return options;
}
function updateCurrentValue(param, options) {
const found = find(options, option => option.value === param.value) !== undefined;
if (!found) {
param.value = options[0].value;
}
}
const QueryBasedParameterComponent = {
template: queryBasedParameterTemplate,
bindings: {
param: '<',
queryId: '<',
},
controller(Query) {
'ngInject';
this.$onChanges = (changes) => {
if (changes.queryId) {
Query.resultById({ id: this.queryId }, (result) => {
const queryResult = result.query_result;
this.queryResultOptions = optionsFromQueryResult(queryResult);
updateCurrentValue(this.param, this.queryResultOptions);
});
}
};
},
};
@@ -40,6 +128,7 @@ function ParametersDirective($location, $uibModal) {
});
}, true);
}
// These are input as newline delimited values,
// so we split them here.
scope.extractEnumOptions = (enumOptions) => {
@@ -60,7 +149,8 @@ function ParametersDirective($location, $uibModal) {
};
}
export default function (ngModule) {
export default function init(ngModule) {
ngModule.directive('parameters', ParametersDirective);
ngModule.component('queryBasedParameter', QueryBasedParameterComponent);
ngModule.component('parameterSettings', ParameterSettingsComponent);
}

View File

@@ -15,7 +15,7 @@ const PermissionsEditorComponent = {
this.newGrantees = {};
this.aclUrl = this.resolve.aclUrl.url;
// List users that are granted permissions
// List users that are granted permissions
const loadGrantees = () => {
$http.get(this.aclUrl).success((result) => {
this.grantees = [];
@@ -31,7 +31,7 @@ const PermissionsEditorComponent = {
loadGrantees();
// Search for user
// Search for user
this.findUser = (search) => {
if (search === '') {
return;
@@ -46,7 +46,7 @@ const PermissionsEditorComponent = {
}
};
// Add new user to grantees list
// Add new user to grantees list
this.addGrantee = (user) => {
this.newGrantees.selected = undefined;
const body = { access_type: 'modify', user_id: user.id };
@@ -56,10 +56,11 @@ const PermissionsEditorComponent = {
});
};
// Remove user from grantees list
// Remove user from grantees list
this.removeGrantee = (user) => {
const body = { access_type: 'modify', user_id: user.id };
$http({ url: this.aclUrl,
$http({
url: this.aclUrl,
method: 'DELETE',
data: body,
headers: { 'Content-Type': 'application/json' },
@@ -74,6 +75,6 @@ const PermissionsEditorComponent = {
},
};
export default function (ngModule) {
export default function init(ngModule) {
ngModule.component('permissionsEditor', PermissionsEditorComponent);
}

View File

@@ -32,6 +32,6 @@ function alertUnsavedChanges($window) {
};
}
export default function (ngModule) {
export default function init(ngModule) {
ngModule.directive('alertUnsavedChanges', alertUnsavedChanges);
}

View File

@@ -32,6 +32,6 @@ const ApiKeyDialog = {
},
};
export default function (ngModule) {
export default function init(ngModule) {
ngModule.component('apiKeyDialog', ApiKeyDialog);
}

View File

@@ -0,0 +1,17 @@
<div class="modal-header">
<button type="button" class="close" aria-label="Close" ng-click="$ctrl.close()"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title">Embed Code</h4>
</div>
<div class="modal-body">
<h5>IFrame Embed</h5>
<div>
<code>&lt;iframe src="{{ $ctrl.embedUrl }}" width="720" height="391"&gt;&lt;/iframe&gt;</code>
</div>
<span class="text-muted">(height should be adjusted)</span>
<div ng-if="$ctrl.snapshotUrl">
<h5>Image Embed</h5>
<div>
<code style="overflow-wrap:break-word;">{{$ctrl.snapshotUrl}}</code>
</div>
</div>
</div>

View File

@@ -20,6 +20,6 @@ const EmbedCodeDialog = {
template,
};
export default function (ngModule) {
export default function init(ngModule) {
ngModule.component('embedCodeDialog', EmbedCodeDialog);
}

View File

@@ -113,13 +113,12 @@ function queryEditor(QuerySnippet) {
});
$scope.schema.keywords = map(keywords, (v, k) =>
({
name: k,
value: k,
score: 0,
meta: v,
})
);
({
name: k,
value: k,
score: 0,
meta: v,
}));
}
callback(null, $scope.schema.keywords);
},
@@ -134,6 +133,6 @@ function queryEditor(QuerySnippet) {
};
}
export default function (ngModule) {
export default function init(ngModule) {
ngModule.directive('queryEditor', queryEditor);
}

View File

@@ -21,6 +21,6 @@ function queryResultLink() {
};
}
export default function (ngModule) {
export default function init(ngModule) {
ngModule.directive('queryResultLink', queryResultLink);
}

View File

@@ -38,9 +38,9 @@ function queryTimePicker() {
$scope.updateSchedule = () => {
const newSchedule = moment().hour($scope.hour)
.minute($scope.minute)
.utc()
.format('HH:mm');
.minute($scope.minute)
.utc()
.format('HH:mm');
if (newSchedule !== $scope.query.schedule) {
$scope.query.schedule = newSchedule;
@@ -143,7 +143,7 @@ const ScheduleForm = {
template,
};
export default function (ngModule) {
export default function init(ngModule) {
ngModule.directive('queryTimePicker', queryTimePicker);
ngModule.directive('queryRefreshSelect', queryRefreshSelect);
ngModule.component('scheduleDialog', ScheduleForm);

View File

@@ -28,6 +28,6 @@ const SchemaBrowser = {
template,
};
export default function (ngModule) {
export default function init(ngModule) {
ngModule.component('schemaBrowser', SchemaBrowser);
}

View File

@@ -1,6 +1,6 @@
import { find } from 'underscore';
import logoUrl from '@/assets/images/redash_icon_small.png';
import template from './visualization-embed.html';
import logoUrl from '../../assets/images/redash_icon_small.png';
const VisualizationEmbed = {
template,
@@ -22,7 +22,7 @@ const VisualizationEmbed = {
},
};
export default function (ngModule) {
export default function init(ngModule) {
ngModule.component('visualizationEmbed', VisualizationEmbed);
function session($http, $route, Auth) {

View File

@@ -0,0 +1,2 @@
<select ng-model="$ctrl.param.value" class="form-control" ng-options="option.value as option.name for option in $ctrl.queryResultOptions">
</select>

View File

@@ -13,7 +13,7 @@ function QueryLinkController() {
this.link = this.query.getUrl(false, hash);
}
export default function (ngModule) {
export default function init(ngModule) {
ngModule.component('queryLink', {
bindings: {
query: '<',

View File

@@ -11,15 +11,14 @@ function rdTab($location) {
replace: true,
link(scope) {
scope.basePath = scope.basePath || $location.path().substring(1);
scope.$watch(() =>
scope.$parent.selectedTab
, (tab) => {
scope.selectedTab = tab;
});
scope.$watch(
() => scope.$parent.selectedTab,
(tab) => { scope.selectedTab = tab; },
);
},
};
}
export default function (ngModule) {
export default function init(ngModule) {
ngModule.directive('rdTab', rdTab);
}

View File

@@ -10,6 +10,6 @@ const RdTimeAgo = {
'</span>',
};
export default function (ngModule) {
export default function init(ngModule) {
ngModule.component('rdTimeAgo', RdTimeAgo);
}

View File

@@ -25,6 +25,6 @@ function rdTimer() {
};
}
export default function (ngModule) {
export default function init(ngModule) {
ngModule.directive('rdTimer', rdTimer);
}

View File

@@ -1,4 +1,4 @@
export default function (ngModule) {
export default function init(ngModule) {
ngModule.component('routeStatus', {
template: '<overlay ng-if="$ctrl.permissionDenied">You do not have permission to load this page.',

View File

@@ -8,11 +8,11 @@
<li ng-class="{'active': usersPage }" ng-if="showUsersLink"><a href="users">Users</a></li>
<li ng-class="{'active': groupsPage }" ng-if="showGroupsLink"><a href="groups">Groups</a></li>
<li ng-class="{'active': destinationsPage }" ng-if="showDestinationsLink"><a href="destinations">Alert Destinations</a></li>
<li ng-class="{'active': snippetsPage }"><a href="query_snippets">Query Snippets</a></li>
<li ng-class="{'active': snippetsPage }" ng-if="showQuerySnippetsLink"><a href="query_snippets">Query Snippets</a></li>
</ul>
<div ng-transclude>
</div>
</div>
</div>
</div>

View File

@@ -1,24 +1,23 @@
import startsWith from 'underscore.string/startsWith';
import template from './settings-screen.html';
export default function (ngModule) {
ngModule.directive('settingsScreen', $location =>
({
restrict: 'E',
transclude: true,
template,
controller($scope, currentUser) {
$scope.usersPage = startsWith($location.path(), '/users');
$scope.groupsPage = startsWith($location.path(), '/groups');
$scope.dsPage = startsWith($location.path(), '/data_sources');
$scope.destinationsPage = startsWith($location.path(), '/destinations');
$scope.snippetsPage = startsWith($location.path(), '/query_snippets');
export default function init(ngModule) {
ngModule.directive('settingsScreen', $location => ({
restrict: 'E',
transclude: true,
template,
controller($scope, currentUser) {
$scope.usersPage = startsWith($location.path(), '/users');
$scope.groupsPage = startsWith($location.path(), '/groups');
$scope.dsPage = startsWith($location.path(), '/data_sources');
$scope.destinationsPage = startsWith($location.path(), '/destinations');
$scope.snippetsPage = startsWith($location.path(), '/query_snippets');
$scope.showGroupsLink = currentUser.hasPermission('list_users');
$scope.showUsersLink = currentUser.hasPermission('list_users');
$scope.showDsLink = currentUser.hasPermission('admin');
$scope.showDestinationsLink = currentUser.hasPermission('admin');
},
})
);
$scope.showGroupsLink = currentUser.hasPermission('list_users');
$scope.showUsersLink = currentUser.hasPermission('list_users');
$scope.showDsLink = currentUser.hasPermission('admin');
$scope.showDestinationsLink = currentUser.hasPermission('admin');
$scope.showQuerySnippetsLink = currentUser.hasPermission('create_query');
},
}));
}

View File

@@ -1,4 +1,4 @@
export default function (ngModule) {
export default function init(ngModule) {
ngModule.component('sortIcon', {
template: '<span ng-if="$ctrl.showIcon"><i class="fa fa-sort-{{$ctrl.icon}}"></i></span>',
bindings: {

View File

@@ -10,7 +10,7 @@ function controller($location) {
});
}
export default function (ngModule) {
export default function init(ngModule) {
ngModule.component('tabNav', {
template: '<ul class="tab-nav bg-white">' +
'<li ng-repeat="tab in $ctrl.tabs" ng-class="{\'active\': tab.active }"><a ng-href="{{tab.path}}">{{tab.name}}</a></li>' +

View File

@@ -1,4 +1,4 @@
function VisualizationName(Visualization) {
export default function VisualizationName(Visualization) {
return {
restrict: 'E',
scope: {
@@ -7,7 +7,10 @@ function VisualizationName(Visualization) {
template: '{{name}}',
replace: false,
link(scope) {
if (Visualization.visualizations[scope.visualization.type].name !== scope.visualization.name) {
const currentType = scope.visualization.type;
const nameByType = Visualization.visualizations[currentType].name;
const currentName = scope.visualization.name;
if (nameByType !== currentName) {
scope.name = scope.visualization.name;
}
},

111
client/app/config/index.js Normal file
View File

@@ -0,0 +1,111 @@
// This polyfill is needed to support PhantomJS which we use to generate PNGs from embeds.
import 'core-js/fn/typed/array-buffer';
import 'pace-progress';
import debug from 'debug';
import angular from 'angular';
import ngSanitize from 'angular-sanitize';
import ngRoute from 'angular-route';
import ngResource from 'angular-resource';
import uiBootstrap from 'angular-ui-bootstrap';
import uiSelect from 'ui-select';
import ngMessages from 'angular-messages';
import toastr from 'angular-toastr';
import ngUpload from 'angular-base64-upload';
import vsRepeat from 'angular-vs-repeat';
import 'angular-moment';
import 'brace';
import 'angular-ui-ace';
import 'angular-resizable';
import ngGridster from 'angular-gridster';
import { each } from 'underscore';
import '@/lib/sortable';
import * as filters from '@/filters';
import registerDirectives from '@/directives';
import markdownFilter from '@/filters/markdown';
import dateTimeFilter from '@/filters/datetime';
const logger = debug('redash:config');
const requirements = [
ngRoute,
ngResource,
ngSanitize,
uiBootstrap,
ngMessages,
uiSelect,
'angularMoment',
toastr,
'ui.ace',
ngUpload,
'angularResizable',
vsRepeat,
'ui.sortable',
ngGridster.name,
];
const ngModule = angular.module('app', requirements);
function registerAll(context) {
const modules = context
.keys()
.map(context)
.map(module => module.default);
return modules.map(f => f(ngModule));
}
function registerComponents() {
// We repeat this code in other register functions, because if we don't use a literal for the path
// Webpack won't be able to statcily analyze our imports.
const context = require.context('@/components', true, /^((?![\\/]test[\\/]).)*\.js$/);
registerAll(context);
}
function registerServices() {
const context = require.context('@/services', true, /^((?![\\/]test[\\/]).)*\.js$/);
registerAll(context);
}
function registerVisualizations() {
const context = require.context('@/visualizations', true, /^((?![\\/]test[\\/]).)*\.js$/);
registerAll(context);
}
function registerPages() {
const context = require.context('@/pages', true, /^((?![\\/]test[\\/]).)*\.js$/);
const routesCollection = registerAll(context);
routesCollection.forEach((routes) => {
ngModule.config(($routeProvider) => {
each(routes, (route, path) => {
logger('Registering route: %s', path);
// This is a workaround, to make sure app-header and footer are loaded only
// for the authenticated routes.
// We should look into switching to ui-router, that has built in support for
// such things.
route.template = `<app-header></app-header><route-status></route-status>${route.template}<footer></footer>`;
route.authenticated = true;
$routeProvider.when(path, route);
});
});
});
}
function registerFilters() {
each(filters, (filter, name) => {
ngModule.filter(name, () => filter);
});
}
registerDirectives(ngModule);
registerServices();
registerFilters();
markdownFilter(ngModule);
dateTimeFilter(ngModule);
registerComponents();
registerPages();
registerVisualizations(ngModule);
export default ngModule;

View File

@@ -0,0 +1,11 @@
import 'material-design-iconic-font/dist/css/material-design-iconic-font.css';
import 'font-awesome/css/font-awesome.css';
import 'ui-select/dist/select.css';
import 'angular-toastr/dist/angular-toastr.css';
import 'angular-resizable/src/angular-resizable.css';
import 'angular-gridster/dist/angular-gridster.css';
import 'pace-progress/themes/blue/pace-theme-minimal.css';
import '@/assets/css/superflat_redash.css';
import '@/assets/css/redash.css';
import '@/assets/css/main.scss';

View File

@@ -70,7 +70,7 @@ function title($rootScope, Title) {
};
}
export default function (ngModule) {
export default function init(ngModule) {
ngModule.factory('Title', TitleService);
ngModule.directive('title', title);
ngModule.directive('compareTo', compareTo);

View File

@@ -1,15 +1,14 @@
import moment from 'moment';
export default function (ngModule) {
export default function init(ngModule) {
ngModule.filter('toMilliseconds', () => value => value * 1000.0);
ngModule.filter('dateTime', clientConfig =>
function dateTime(value) {
if (!value) {
return '-';
}
function dateTime(value) {
if (!value) {
return '-';
}
return moment(value).format(clientConfig.dateTimeFormat);
}
);
return moment(value).format(clientConfig.dateTimeFormat);
});
}

View File

@@ -32,10 +32,10 @@ export function scheduleHumanize(schedule) {
} else if (schedule.match(/\d\d:\d\d/) !== null) {
const parts = schedule.split(':');
const localTime = moment.utc()
.hour(parts[0])
.minute(parts[1])
.local()
.format('HH:mm');
.hour(parts[0])
.minute(parts[1])
.local()
.format('HH:mm');
return `Every day at ${localTime}`;
}
@@ -45,8 +45,7 @@ export function scheduleHumanize(schedule) {
export function toHuman(text) {
return text.replace(/_/g, ' ').replace(/(?:^|\s)\S/g, a =>
a.toUpperCase()
);
a.toUpperCase());
}
export function colWidth(widgetWidth) {

View File

@@ -1,18 +1,17 @@
import marked from 'marked';
import { markdown } from 'markdown';
export default function (ngModule) {
export default function init(ngModule) {
ngModule.filter('markdown', ($sce, clientConfig) =>
function markdown(text) {
if (!text) {
return '';
}
function parseMarkdown(text) {
if (!text) {
return '';
}
let html = marked(String(text));
if (clientConfig.allowScriptsInUserInput) {
html = $sce.trustAsHtml(html);
}
let html = markdown.toHTML(String(text));
if (clientConfig.allowScriptsInUserInput) {
html = $sce.trustAsHtml(html);
}
return html;
}
);
return html;
});
}

View File

@@ -1,105 +1,7 @@
// This polyfill is needed to support PhantomJS which we use to generate PNGs from embeds.
import 'core-js/fn/typed/array-buffer';
import '@/config/styles';
import ngModule from '@/config';
import 'material-design-iconic-font/dist/css/material-design-iconic-font.css';
import 'font-awesome/css/font-awesome.css';
import 'ui-select/dist/select.css';
import 'angular-toastr/dist/angular-toastr.css';
import 'angular-resizable/src/angular-resizable.css';
import 'angular-gridster/dist/angular-gridster.css';
import 'pace-progress/themes/blue/pace-theme-minimal.css';
import 'pace-progress';
import debug from 'debug';
import angular from 'angular';
import ngSanitize from 'angular-sanitize';
import ngRoute from 'angular-route';
import ngResource from 'angular-resource';
import uiBootstrap from 'angular-ui-bootstrap';
import uiSelect from 'ui-select';
import ngMessages from 'angular-messages';
import toastr from 'angular-toastr';
import ngUpload from 'angular-base64-upload';
import vsRepeat from 'angular-vs-repeat';
import 'angular-moment';
import 'brace';
import 'angular-ui-ace';
import 'angular-resizable';
import ngGridster from 'angular-gridster';
import { each } from 'underscore';
import './sortable';
import './assets/css/superflat_redash.css';
import './assets/css/redash.css';
import './assets/css/main.scss';
import * as pages from './pages';
import * as components from './components';
import * as filters from './filters';
import * as services from './services';
import registerDirectives from './directives';
import registerVisualizations from './visualizations';
import markdownFilter from './filters/markdown';
import dateTimeFilter from './filters/datetime';
const logger = debug('redash');
const requirements = [
ngRoute, ngResource, ngSanitize, uiBootstrap, ngMessages, uiSelect, 'angularMoment', toastr, 'ui.ace',
ngUpload, 'angularResizable', vsRepeat, 'ui.sortable', ngGridster.name,
];
const ngModule = angular.module('app', requirements);
function registerComponents() {
each(components, (register) => {
register(ngModule);
});
}
function registerServices() {
each(services, (register) => {
register(ngModule);
});
}
function registerPages() {
each(pages, (registerPage) => {
const routes = registerPage(ngModule);
ngModule.config(($routeProvider) => {
each(routes, (route, path) => {
logger('Route: ', path);
// This is a workaround, to make sure app-header and footer are loaded only
// for the authenticated routes.
// We should look into switching to ui-router, that has built in support for
// such things.
route.template = `<app-header></app-header><route-status></route-status>${route.template}<footer></footer>`;
route.authenticated = true;
$routeProvider.when(path, route);
});
});
});
}
function registerFilters() {
each(filters, (filter, name) => {
ngModule.filter(name, () => filter);
});
}
registerDirectives(ngModule);
registerServices();
registerFilters();
markdownFilter(ngModule);
dateTimeFilter(ngModule);
registerComponents();
registerPages();
registerVisualizations(ngModule);
ngModule.config(($routeProvider, $locationProvider, $compileProvider,
uiSelectConfig, toastrConfig) => {
ngModule.config(($locationProvider, $compileProvider, uiSelectConfig, toastrConfig) => {
$compileProvider.debugInfoEnabled(false);
$compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|http|data):/);
$locationProvider.html5Mode(true);
@@ -112,7 +14,8 @@ ngModule.config(($routeProvider, $locationProvider, $compileProvider,
});
// Update ui-select's template to use Font-Awesome instead of glyphicon.
ngModule.run(($templateCache, OfflineListener) => { // eslint-disable-line no-unused-vars
// eslint-disable-next-line no-unused-vars
ngModule.run(($templateCache, OfflineListener) => {
const templateName = 'bootstrap/match.tpl.html';
let template = $templateCache.get(templateName);
template = template.replace('glyphicon glyphicon-remove', 'fa fa-remove');

328
client/app/lib/visualizations/d3box.js vendored Normal file
View File

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

View File

@@ -10,7 +10,7 @@ function value(link) {
return link.value;
}
export default function() {
function Sankey() {
const sankey = {};
let nodeWidth = 24;
let nodePadding = 8;
@@ -21,11 +21,11 @@ export default function() {
// Populate the sourceLinks and targetLinks for each node.
// Also, if the source and target are not objects, assume they are indices.
function computeNodeLinks() {
nodes.forEach((node) => {
nodes.forEach(node => {
node.sourceLinks = [];
node.targetLinks = [];
});
links.forEach((link) => {
links.forEach(link => {
let source = link.source;
let target = link.target;
if (typeof source === 'number') source = link.source = nodes[link.source];
@@ -37,16 +37,13 @@ export default function() {
// Compute the value (size) of each node by summing the associated links.
function computeNodeValues() {
nodes.forEach((node) => {
node.value = Math.max(
d3.sum(node.sourceLinks, value),
d3.sum(node.targetLinks, value)
);
nodes.forEach(node => {
node.value = Math.max(d3.sum(node.sourceLinks, value), d3.sum(node.targetLinks, value));
});
}
function moveSinksRight(x) {
nodes.forEach((node) => {
nodes.forEach(node => {
if (!node.sourceLinks.length) {
node.x = x - 1;
}
@@ -54,7 +51,7 @@ export default function() {
}
function scaleNodeBreadths(kx) {
nodes.forEach((node) => {
nodes.forEach(node => {
node.x *= kx;
});
}
@@ -71,7 +68,7 @@ export default function() {
function assignBreadth(node) {
node.x = x;
node.dx = nodeWidth;
node.sourceLinks.forEach((link) => {
node.sourceLinks.forEach(link => {
if (nextNodes.indexOf(link.target) < 0) {
nextNodes.push(link.target);
}
@@ -91,7 +88,7 @@ export default function() {
}
function moveSourcesRight() {
nodes.forEach((node) => {
nodes.forEach(node => {
if (!node.targetLinks.length) {
node.x = d3.min(node.sourceLinks, d => d.target.x) - 1;
}
@@ -99,25 +96,27 @@ export default function() {
}
function computeNodeDepths(iterations) {
const nodesByBreadth = d3.nest()
.key(d => d.x)
.sortKeys(d3.ascending)
.entries(nodes)
.map(d => d.values);
const nodesByBreadth = d3
.nest()
.key(d => d.x)
.sortKeys(d3.ascending)
.entries(nodes)
.map(d => d.values);
function initializeNodeDepth() {
const ky = d3.min(nodesByBreadth, n =>
(size[1] - (n.length - 1) * nodePadding) / d3.sum(n, value)
const ky = d3.min(
nodesByBreadth,
n => (size[1] - (n.length - 1) * nodePadding) / d3.sum(n, value),
);
nodesByBreadth.forEach((n) => {
nodesByBreadth.forEach(n => {
n.forEach((node, i) => {
node.y = i;
node.dy = node.value * ky;
});
});
links.forEach((link) => {
links.forEach(link => {
link.dy = link.value * ky;
});
}
@@ -127,8 +126,8 @@ export default function() {
return center(link.source) * link.value;
}
nodesByBreadth.forEach((n) => {
n.forEach((node) => {
nodesByBreadth.forEach(n => {
n.forEach(node => {
if (node.targetLinks.length) {
const y = d3.sum(node.targetLinks, weightedSource) / d3.sum(node.targetLinks, value);
node.y += (y - center(node)) * alpha;
@@ -138,7 +137,7 @@ export default function() {
}
function resolveCollisions() {
nodesByBreadth.forEach((nodes) => {
nodesByBreadth.forEach(nodes => {
const n = nodes.length;
let node;
let dy;
@@ -171,7 +170,7 @@ export default function() {
}
function resolveCollisions() {
nodesByBreadth.forEach((nodes) => {
nodesByBreadth.forEach(nodes => {
let node,
dy,
y0 = 0,
@@ -203,26 +202,28 @@ export default function() {
});
}
initializeNodeDepth();
resolveCollisions();
for (let alpha = 1; iterations > 0; iterations -= 1) {
relaxRightToLeft(alpha *= 0.99);
relaxRightToLeft((alpha *= 0.99));
resolveCollisions();
relaxLeftToRight(alpha);
resolveCollisions();
}
function relaxRightToLeft(alpha) {
nodesByBreadth.slice().reverse().forEach((nodes) => {
nodes.forEach((node) => {
if (node.sourceLinks.length) {
const y = d3.sum(node.sourceLinks, weightedTarget) / d3.sum(node.sourceLinks, value);
node.y += (y - center(node)) * alpha;
}
nodesByBreadth
.slice()
.reverse()
.forEach(nodes => {
nodes.forEach(node => {
if (node.sourceLinks.length) {
const y = d3.sum(node.sourceLinks, weightedTarget) / d3.sum(node.sourceLinks, value);
node.y += (y - center(node)) * alpha;
}
});
});
});
function weightedTarget(link) {
return center(link.target) * link.value;
@@ -235,18 +236,18 @@ export default function() {
}
function computeLinkDepths() {
nodes.forEach((node) => {
nodes.forEach(node => {
node.sourceLinks.sort(ascendingTargetDepth);
node.targetLinks.sort(ascendingSourceDepth);
});
nodes.forEach((node) => {
nodes.forEach(node => {
let sy = 0,
ty = 0;
node.sourceLinks.forEach((link) => {
node.sourceLinks.forEach(link => {
link.sy = sy;
sy += link.dy;
});
node.targetLinks.forEach((link) => {
node.targetLinks.forEach(link => {
link.ty = ty;
ty += link.dy;
});
@@ -261,37 +262,37 @@ export default function() {
}
}
sankey.nodeWidth = function (_) {
sankey.nodeWidth = function(_) {
if (!arguments.length) return nodeWidth;
nodeWidth = +_;
return sankey;
};
sankey.nodePadding = function (_) {
sankey.nodePadding = function(_) {
if (!arguments.length) return nodePadding;
nodePadding = +_;
return sankey;
};
sankey.nodes = function (_) {
sankey.nodes = function(_) {
if (!arguments.length) return nodes;
nodes = _;
return sankey;
};
sankey.links = function (_) {
sankey.links = function(_) {
if (!arguments.length) return links;
links = _;
return sankey;
};
sankey.size = function (_) {
sankey.size = function(_) {
if (!arguments.length) return size;
size = _;
return sankey;
};
sankey.layout = function (iterations) {
sankey.layout = function(iterations) {
computeNodeLinks();
computeNodeValues();
computeNodeBreadths();
@@ -300,12 +301,12 @@ export default function() {
return sankey;
};
sankey.relayout = function () {
sankey.relayout = function() {
computeLinkDepths();
return sankey;
};
sankey.link = function () {
sankey.link = function() {
let curvature = 0.5;
function link(d) {
@@ -317,13 +318,10 @@ export default function() {
const y0 = d.source.y + d.sy + d.dy / 2;
const y1 = d.target.y + d.ty + d.dy / 2;
return `M${x0},${y0
}C${x2},${y0
} ${x3},${y1
} ${x1},${y1}`;
return `M${x0},${y0}C${x2},${y0} ${x3},${y1} ${x1},${y1}`;
}
link.curvature = (_) => {
link.curvature = _ => {
if (!arguments.length) return curvature;
curvature = +_;
return link;
@@ -332,6 +330,7 @@ export default function() {
return link;
};
return sankey;
};
}
export default Sankey;

View File

@@ -1,4 +1,4 @@
import d3 from 'd3';
import * as d3 from 'd3';
import _ from 'underscore';
import angular from 'angular';
@@ -10,7 +10,6 @@ function colorMap(d) {
return colors(d.name);
}
// Return array of ancestors of nodes, highest first, but excluding the root.
function getAncestors(node) {
const path = [];
@@ -24,7 +23,7 @@ function getAncestors(node) {
}
// The following is based on @chrisrzhou's example from: http://bl.ocks.org/chrisrzhou/d5bdd8546f64ca0e4366.
export default function Sunburst(scope, element) {
function Sunburst(scope, element) {
this.element = element;
this.watches = [];
@@ -60,27 +59,18 @@ export default function Sunburst(scope, element) {
let totalSize = 0;
// create d3.layout.partition
const partition = d3.layout.partition()
const partition = d3.layout
.partition()
.size([2 * Math.PI, radius * radius])
.value(d =>
d.size
);
.value(d => d.size);
// create arcs for drawing D3 paths
const arc = d3.svg.arc()
.startAngle(d =>
d.x
)
.endAngle(d =>
d.x + d.dx
)
.innerRadius(d =>
Math.sqrt(d.y)
)
.outerRadius(d =>
Math.sqrt(d.y + d.dy)
);
const arc = d3.svg
.arc()
.startAngle(d => d.x)
.endAngle(d => d.x + d.dx)
.innerRadius(d => Math.sqrt(d.y))
.outerRadius(d => Math.sqrt(d.y + d.dy));
/**
* Define and initialize D3 select references and div-containers
@@ -88,15 +78,18 @@ export default function Sunburst(scope, element) {
* e.g. vis, breadcrumbs, lastCrumb, summary, sunburst, legend
*/
// create main vis selection
const vis = d3.select(element[0])
.append('div').classed('vis-container', true)
const vis = d3
.select(element[0])
.append('div')
.classed('vis-container', true)
.style('position', 'relative')
.style('margin-top', '5px')
.style('height', `${height + 2 * b.h}px`);
// create and position breadcrumbs container and svg
const breadcrumbs = vis
.append('div').classed('breadcrumbs-container', true)
.append('div')
.classed('breadcrumbs-container', true)
.append('svg')
.attr('width', width)
.attr('height', b.h)
@@ -107,7 +100,8 @@ export default function Sunburst(scope, element) {
// create and position SVG
const sunburst = vis
.append('div').classed('sunburst-container', true)
.append('div')
.classed('sunburst-container', true)
.style('z-index', '2')
// .style("margin-left", marginLeft + "px")
.style('left', `${marginLeft}px`)
@@ -123,9 +117,10 @@ export default function Sunburst(scope, element) {
// create and position summary container
const summary = vis
.append('div').classed('summary-container', true)
.append('div')
.classed('summary-container', true)
.style('position', 'absolute')
.style('top', `${b.h + radius * 0.80}px`)
.style('top', `${b.h + radius * 0.8}px`)
.style('left', `${marginLeft + radius / 2}px`)
.style('width', `${radius}px`)
.style('height', `${radius}px`)
@@ -143,7 +138,8 @@ export default function Sunburst(scope, element) {
points.push(`${b.w},${b.h}`);
points.push(`0,${b.h}`);
if (i > 0) { // Leftmost breadcrumb; don't include 6th vertex.
if (i > 0) {
// Leftmost breadcrumb; don't include 6th vertex.
points.push(`${b.t},${b.h / 2}`);
}
return points.join(' ');
@@ -152,34 +148,29 @@ export default function Sunburst(scope, element) {
// Update the breadcrumb breadcrumbs to show the current sequence and percentage.
function updateBreadcrumbs(ancestors, percentageString) {
// Data join, where primary key = name + depth.
const g = breadcrumbs.selectAll('g')
.data(ancestors, d =>
d.name + d.depth
);
const g = breadcrumbs.selectAll('g').data(ancestors, d => d.name + d.depth);
// Add breadcrumb and label for entering nodes.
const breadcrumb = g.enter().append('g');
breadcrumb
.append('polygon').classed('breadcrumbs-shape', true)
.append('polygon')
.classed('breadcrumbs-shape', true)
.attr('points', breadcrumbPoints)
.attr('fill', colorMap);
breadcrumb
.append('text').classed('breadcrumbs-text', true)
.append('text')
.classed('breadcrumbs-text', true)
.attr('x', (b.w + b.t) / 2)
.attr('y', b.h / 2)
.attr('dy', '0.35em')
.attr('font-size', '10px')
.attr('text-anchor', 'middle')
.text(d =>
d.name
);
.text(d => d.name);
// Set position for entering and updating nodes.
g.attr('transform', (d, i) =>
`translate(${i * (b.w + b.s)}, 0)`
);
g.attr('transform', (d, i) => `translate(${i * (b.w + b.s)}, 0)`);
// Remove exiting nodes.
g.exit().remove();
@@ -210,32 +201,27 @@ export default function Sunburst(scope, element) {
updateBreadcrumbs(ancestors, percentageString);
// update sunburst (Fade all the segments and highlight only ancestors of current segment)
sunburst.selectAll('path')
.attr('opacity', 0.3);
sunburst.selectAll('path')
.filter(node =>
(ancestors.indexOf(node) >= 0)
)
sunburst.selectAll('path').attr('opacity', 0.3);
sunburst
.selectAll('path')
.filter(node => ancestors.indexOf(node) >= 0)
.attr('opacity', 1);
// update summary
summary.html(
`Stage: ${d.depth}<br />` +
`<span class='percentage' style='font-size: 2em;'>${percentageString}</span><br />${
d.value} of ${totalSize}<br />`
);
summary.html(`Stage: ${d.depth}<br />` +
`<span class='percentage' style='font-size: 2em;'>${percentageString}</span><br />${d.value} of ${totalSize}<br />`);
// display summary and breadcrumbs if hidden
summary.style('visibility', '');
breadcrumbs.style('visibility', '');
}
// helper function click to handle mouseleave events/animations
function click() {
// Deactivate all segments then retransition each segment to full opacity.
sunburst.selectAll('path').on('mouseover', null);
sunburst.selectAll('path')
sunburst
.selectAll('path')
.transition()
.duration(1000)
.attr('opacity', 1)
@@ -251,10 +237,8 @@ export default function Sunburst(scope, element) {
// helper function to draw the sunburst and breadcrumbs
function drawSunburst(json) {
// Build only nodes of a threshold "visible" sizes to improve efficiency
const nodes = partition.nodes(json)
.filter(d =>
(d.dx > 0.005) && d.name !== exitNode // 0.005 radians = 0.29 degrees
);
// 0.005 radians = 0.29 degrees
const nodes = partition.nodes(json).filter(d => d.dx > 0.005 && d.name !== exitNode);
// this section is required to update the colors.domain() every time the data updates
const uniqueNames = (function uniqueNames(a) {
@@ -267,8 +251,11 @@ export default function Sunburst(scope, element) {
colors.domain(uniqueNames); // update domain colors
// create path based on nodes
const path = sunburst.data([json]).selectAll('path')
.data(nodes).enter()
const path = sunburst
.data([json])
.selectAll('path')
.data(nodes)
.enter()
.append('path')
.classed('nodePath', true)
.attr('display', d => (d.depth ? null : 'none'))
@@ -278,7 +265,6 @@ export default function Sunburst(scope, element) {
.attr('stroke', 'white')
.on('mouseover', mouseover);
// // trigger mouse click over sunburst to reset visualization summary
vis.on('click', click);
@@ -299,7 +285,12 @@ export default function Sunburst(scope, element) {
function buildNodes(raw) {
let values;
if (_.has(raw[0], 'sequence') && _.has(raw[0], 'stage') && _.has(raw[0], 'node') && _.has(raw[0], 'value')) {
if (
_.has(raw[0], 'sequence') &&
_.has(raw[0], 'stage') &&
_.has(raw[0], 'node') &&
_.has(raw[0], 'value')
) {
const grouped = _.groupBy(raw, 'sequence');
values = _.map(grouped, (value) => {
@@ -314,13 +305,11 @@ export default function Sunburst(scope, element) {
const validKey = key => key !== 'value' && key.indexOf('$$') !== 0;
const keys = _.sortBy(_.filter(_.keys(raw[0]), validKey), _.identity);
values = _.map(raw, (row, sequence) =>
({
size: row.value,
sequence,
nodes: _.compact(_.map(keys, key => row[key])),
})
);
values = _.map(raw, (row, sequence) => ({
size: row.value,
sequence,
nodes: _.compact(_.map(keys, key => row[key])),
}));
}
return values;
@@ -346,7 +335,6 @@ export default function Sunburst(scope, element) {
const nodeName = nodes[j];
const isLeaf = j + 1 === nodes.length;
if (!children) {
currentNode.children = children = [];
children.push({
@@ -403,6 +391,10 @@ export default function Sunburst(scope, element) {
}
Sunburst.prototype.remove = function remove() {
this.watches.forEach((unregister) => { unregister(); });
this.watches.forEach((unregister) => {
unregister();
});
angular.element(this.element[0]).empty('.vis-container');
};
export default Sunburst;

View File

@@ -1,10 +0,0 @@
import registerStatusPage from './status';
import registerOutdatedQueriesPage from './outdated-queries';
import registerTasksPage from './tasks';
export default function (ngModule) {
const routes = Object.assign({}, registerStatusPage(ngModule),
registerOutdatedQueriesPage(ngModule),
registerTasksPage(ngModule));
return routes;
}

View File

@@ -1,6 +1,6 @@
import moment from 'moment';
import { Paginator } from '../../../utils';
import { Paginator } from '@/lib/pagination';
import template from './outdated-queries.html';
function OutdatedQueriesCtrl($scope, Events, $http, $timeout) {
@@ -30,7 +30,7 @@ function OutdatedQueriesCtrl($scope, Events, $http, $timeout) {
refresh();
}
export default function (ngModule) {
export default function init(ngModule) {
ngModule.component('outdatedQueriesPage', {
template,
controller: OutdatedQueriesCtrl,

View File

@@ -25,7 +25,7 @@ function AdminStatusCtrl($scope, $http, $timeout, currentUser, Events) {
refresh();
}
export default function (ngModule) {
export default function init(ngModule) {
ngModule.component('statusPage', {
template,
controller: AdminStatusCtrl,

View File

@@ -1,8 +1,7 @@
import moment from 'moment';
import { Paginator } from '../../../utils';
import { Paginator } from '@/lib/pagination';
import template from './tasks.html';
import registerCancelQueryButton from './cancel-query-button';
function TasksCtrl($scope, $location, $http, $timeout, Events) {
Events.record('view', 'page', 'admin/tasks');
@@ -46,14 +45,12 @@ function TasksCtrl($scope, $location, $http, $timeout, Events) {
refresh();
}
export default function (ngModule) {
export default function init(ngModule) {
ngModule.component('tasksPage', {
template,
controller: TasksCtrl,
});
registerCancelQueryButton(ngModule);
return {
'/admin/queries/tasks': {
template: '<tasks-page></tasks-page>',

View File

@@ -42,7 +42,7 @@
<td>{{row.started_at | toMilliseconds | dateTime }}</td>
<td>{{row.updated_at | toMilliseconds | dateTime }}</td>
<td ng-if="selectedTab === 'in_progress'">
<cancel-query-button query-id="dataRow.query_id" task-id="dataRow.task_id"></cancel-query-button>
<cancel-query-button query-id="row.query_id" task-id="row.task_id"></cancel-query-button>
</td>
</tr>
</tbody>

View File

@@ -1,102 +0,0 @@
import { contains, without, compact } from 'underscore';
import template from './alert-subscriptions.html';
function controller($scope, $q, $sce, currentUser, AlertSubscription, Destination, toastr) {
'ngInject';
$scope.newSubscription = {};
$scope.subscribers = [];
$scope.destinations = [];
$scope.currentUser = currentUser;
$q.all([Destination.query().$promise,
AlertSubscription.query({ alertId: $scope.alertId }).$promise]).then((responses) => {
const destinations = responses[0];
const subscribers = responses[1];
const subscribedDestinations =
compact(subscribers.map(s => s.destination && s.destination.id));
const subscribedUsers =
compact(subscribers.map(s => !s.destination && s.user.id));
$scope.destinations = destinations.filter(d => !contains(subscribedDestinations, d.id));
if (!contains(subscribedUsers, currentUser.id)) {
$scope.destinations.unshift({ user: { name: currentUser.name } });
}
$scope.newSubscription.destination = $scope.destinations[0];
$scope.subscribers = subscribers;
});
$scope.destinationsDisplay = (d) => {
if (!d) {
return '';
}
let destination = d;
if (d.destination) {
destination = destination.destination;
} else if (destination.user) {
destination = {
name: `${d.user.name} (Email)`,
icon: 'fa-envelope',
type: 'user',
};
}
return $sce.trustAsHtml(`<i class="fa ${destination.icon}"></i>&nbsp;${destination.name}`);
};
$scope.saveSubscriber = () => {
const sub = new AlertSubscription({ alert_id: $scope.alertId });
if ($scope.newSubscription.destination.id) {
sub.destination_id = $scope.newSubscription.destination.id;
}
sub.$save(() => {
toastr.success('Subscribed.');
$scope.subscribers.push(sub);
$scope.destinations = without($scope.destinations, $scope.newSubscription.destination);
if ($scope.destinations.length > 0) {
$scope.newSubscription.destination = $scope.destinations[0];
} else {
$scope.newSubscription.destination = undefined;
}
}, () => {
toastr.error('Failed saving subscription.');
});
};
$scope.unsubscribe = (subscriber) => {
const destination = subscriber.destination;
const user = subscriber.user;
subscriber.$delete(() => {
toastr.success('Unsubscribed');
$scope.subscribers = without($scope.subscribers, subscriber);
if (destination) {
$scope.destinations.push(destination);
} else if (user.id === currentUser.id) {
$scope.destinations.push({ user: { name: currentUser.name } });
}
if ($scope.destinations.length === 1) {
$scope.newSubscription.destination = $scope.destinations[0];
}
}, () => {
toastr.error('Failed unsubscribing.');
});
};
}
export default () => ({
restrict: 'E',
replace: true,
scope: {
alertId: '=',
},
template,
controller,
});

View File

@@ -1,6 +1,5 @@
import { template as templateBuilder } from 'underscore';
import template from './alert.html';
import alertSubscriptions from './alert-subscriptions';
function AlertCtrl($routeParams, $location, $sce, toastr, currentUser, Query, Events, Alert) {
this.alertId = $routeParams.alertId;
@@ -60,34 +59,38 @@ function AlertCtrl($routeParams, $location, $sce, toastr, currentUser, Query, Ev
if (this.alert.rearm === '' || this.alert.rearm === 0) {
this.alert.rearm = null;
}
this.alert.$save((alert) => {
toastr.success('Saved.');
if (this.alertId === 'new') {
$location.path(`/alerts/${alert.id}`).replace();
}
}, () => {
toastr.error('Failed saving alert.');
});
this.alert.$save(
(alert) => {
toastr.success('Saved.');
if (this.alertId === 'new') {
$location.path(`/alerts/${alert.id}`).replace();
}
},
() => {
toastr.error('Failed saving alert.');
},
);
};
this.delete = () => {
this.alert.$delete(() => {
$location.path('/alerts');
toastr.success('Alert deleted.');
}, () => {
toastr.error('Failed deleting alert.');
});
this.alert.$delete(
() => {
$location.path('/alerts');
toastr.success('Alert deleted.');
},
() => {
toastr.error('Failed deleting alert.');
},
);
};
}
export default function (ngModule) {
export default function init(ngModule) {
ngModule.component('alertPage', {
template,
controller: AlertCtrl,
});
ngModule.directive('alertSubscriptions', alertSubscriptions);
return {
'/alerts/:alertId': {
template: '<alert-page></alert-page>',

View File

@@ -1,4 +1,4 @@
import { Paginator } from '../../utils';
import { Paginator } from '@/lib/pagination';
import template from './alerts-list.html';
const stateClass = {
@@ -26,7 +26,7 @@ class AlertsListCtrl {
}
}
export default function (ngModule) {
export default function init(ngModule) {
ngModule.component('alertsListPage', {
template,
controller: AlertsListCtrl,

View File

@@ -2,7 +2,7 @@
<page-header title="Dashboards"></page-header>
<div class="col-lg-3">
<input type='text' class='form-control' placeholder="Search Dashboards..."
ng-change="$ctrl.update()" ng-model="$ctrl.searchText"/>
ng-change="$ctrl.update()" ng-model="$ctrl.searchText" autofocus/>
<div class='list-group m-t-20 tags-list'>
<a ng-repeat='tag in $ctrl.allTags' ng-class='{"active": $ctrl.tagIsSelected(tag)}'
class='list-group-item' ng-click='$ctrl.toggleTag($event, tag)'>
@@ -34,4 +34,4 @@
<paginator paginator="$ctrl.paginator"></paginator>
</div>
</div>
</div>
</div>

View File

@@ -1,6 +1,6 @@
import _ from 'underscore';
import { Paginator } from '../../utils';
import { Paginator } from '@/lib/pagination';
import template from './dashboard-list.html';
import './dashboard-list.css';
@@ -75,7 +75,7 @@ function DashboardListCtrl(Dashboard, $location, clientConfig) {
this.update();
}
export default function (ngModule) {
export default function init(ngModule) {
ngModule.component('pageDashboardList', {
template,
controller: DashboardListCtrl,

View File

@@ -2,8 +2,10 @@ import * as _ from 'underscore';
import template from './dashboard.html';
import shareDashboardTemplate from './share-dashboard.html';
function DashboardCtrl($rootScope, $routeParams, $location, $timeout, $q, $uibModal,
Title, AlertDialog, Dashboard, currentUser, clientConfig, Events) {
function DashboardCtrl(
$rootScope, $routeParams, $location, $timeout, $q, $uibModal,
Title, AlertDialog, Dashboard, currentUser, clientConfig, Events,
) {
this.isFullscreen = false;
this.refreshRate = null;
this.showPermissionsControl = clientConfig.showPermissionsControl;
@@ -42,8 +44,7 @@ function DashboardCtrl($rootScope, $routeParams, $location, $timeout, $q, $uibMo
globalParams[param.name].locals.push(param);
});
}
})
);
}));
this.globalParameters = _.values(globalParams);
};
@@ -60,16 +61,15 @@ function DashboardCtrl($rootScope, $routeParams, $location, $timeout, $q, $uibMo
const promises = [];
this.dashboard.widgets.forEach(row =>
row.forEach((widget) => {
if (widget.visualization) {
const maxAge = force ? 0 : undefined;
const queryResult = widget.getQuery().getQueryResult(maxAge);
if (!_.isUndefined(queryResult)) {
promises.push(queryResult.toPromise());
}
}
})
);
row.forEach((widget) => {
if (widget.visualization) {
const maxAge = force ? 0 : undefined;
const queryResult = widget.getQuery().getQueryResult(maxAge);
if (!_.isUndefined(queryResult)) {
promises.push(queryResult.toPromise());
}
}
}));
this.extractGlobalParameters();
@@ -115,10 +115,10 @@ function DashboardCtrl($rootScope, $routeParams, $location, $timeout, $q, $uibMo
Events.record('view', 'dashboard', dashboard.id);
renderDashboard(dashboard, force);
}, () => {
// 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.
// 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.
this.loadDashboard();
});
}, 1000);
@@ -128,8 +128,7 @@ function DashboardCtrl($rootScope, $routeParams, $location, $timeout, $q, $uibMo
this.autoRefresh = () => {
$timeout(() => {
this.loadDashboard(true);
}, this.refreshRate.rate * 1000
).then(() => this.autoRefresh());
}, this.refreshRate.rate * 1000).then(() => this.autoRefresh());
};
this.archiveDashboard = () => {
@@ -260,7 +259,7 @@ const ShareDashboardComponent = {
},
};
export default function (ngModule) {
export default function init(ngModule) {
ngModule.component('shareDashboard', ShareDashboardComponent);
ngModule.component('dashboardPage', {
template,

View File

@@ -1,14 +0,0 @@
import dashboardPage from './dashboard';
import dashboardList from './dashboard-list';
import widgetComponent from './widget';
import addWidgetDialog from './add-widget-dialog';
import registerEditDashboardDialog from './edit-dashboard-dialog';
import publicDashboardPage from './public-dashboard-page';
export default function (ngModule) {
addWidgetDialog(ngModule);
widgetComponent(ngModule);
publicDashboardPage(ngModule);
registerEditDashboardDialog(ngModule);
return Object.assign({}, dashboardPage(ngModule), dashboardList(ngModule));
}

View File

@@ -1,5 +1,5 @@
import logoUrl from '@/assets/images/redash_icon_small.png';
import template from './public-dashboard-page.html';
import logoUrl from '../../assets/images/redash_icon_small.png';
const PublicDashboardPage = {
template,
@@ -17,23 +17,18 @@ const PublicDashboardPage = {
}
this.public = true;
this.dashboard.widgets = this.dashboard.widgets.map(row =>
row.map(widget =>
new Widget(widget)
)
);
row.map(widget => new Widget(widget)));
},
};
export default function (ngModule) {
export default function init(ngModule) {
ngModule.component('publicDashboardPage', PublicDashboardPage);
function loadPublicDashboard($http, $route) {
'ngInject';
const token = $route.current.params.token;
return $http.get(`api/dashboards/public/${token}`).then(response =>
response.data
);
return $http.get(`api/dashboards/public/${token}`).then(response => response.data);
}
function session($http, $route, Auth) {
@@ -52,4 +47,6 @@ export default function (ngModule) {
},
});
});
return [];
}

View File

@@ -1,6 +0,0 @@
import registerList from './list';
import registerShow from './show';
export default function (ngModule) {
return Object.assign({}, registerList(ngModule), registerShow(ngModule));
}

View File

@@ -6,7 +6,7 @@ function DataSourcesCtrl($scope, $location, currentUser, Events, DataSource) {
$scope.dataSources = DataSource.query();
}
export default function (ngModule) {
export default function init(ngModule) {
ngModule.controller('DataSourcesCtrl', DataSourcesCtrl);
return {

View File

@@ -3,8 +3,10 @@ import template from './show.html';
const logger = debug('redash:http');
function DataSourceCtrl($scope, $routeParams, $http, $location, toastr,
currentUser, Events, DataSource) {
function DataSourceCtrl(
$scope, $routeParams, $http, $location, toastr,
currentUser, Events, DataSource,
) {
Events.record('view', 'page', 'admin/data_source');
$scope.dataSourceId = $routeParams.dataSourceId;
@@ -52,11 +54,13 @@ function DataSourceCtrl($scope, $routeParams, $http, $location, toastr,
$scope.actions = [
{ name: 'Delete', class: 'btn-danger', callback: deleteDataSource },
{ name: 'Test Connection', class: 'btn-default', callback: testConnection, disableWhenDirty: true },
{
name: 'Test Connection', class: 'btn-default', callback: testConnection, disableWhenDirty: true,
},
];
}
export default function (ngModule) {
export default function init(ngModule) {
ngModule.controller('DataSourceCtrl', DataSourceCtrl);
return {

View File

@@ -1,6 +0,0 @@
import registerList from './list';
import registerShow from './show';
export default function (ngModule) {
return Object.assign({}, registerList(ngModule), registerShow(ngModule));
}

View File

@@ -6,7 +6,7 @@ function DestinationsCtrl($scope, $location, toastr, currentUser, Events, Destin
$scope.destinations = Destination.query();
}
export default function (ngModule) {
export default function init(ngModule) {
ngModule.controller('DestinationsCtrl', DestinationsCtrl);
return {

View File

@@ -4,8 +4,10 @@ import template from './show.html';
const logger = debug('redash:http');
function DestinationCtrl($scope, $routeParams, $http, $location, toastr,
currentUser, Events, Destination) {
function DestinationCtrl(
$scope, $routeParams, $http, $location, toastr,
currentUser, Events, Destination,
) {
Events.record('view', 'page', 'admin/destination');
$scope.destinationId = $routeParams.destinationId;
@@ -35,7 +37,7 @@ function DestinationCtrl($scope, $routeParams, $http, $location, toastr,
};
}
export default function (ngModule) {
export default function init(ngModule) {
ngModule.controller('DestinationCtrl', DestinationCtrl);
return {

View File

@@ -43,7 +43,7 @@ function GroupDataSourcesCtrl($scope, $routeParams, $http, Events, Group, DataSo
};
}
export default function (ngModule) {
export default function init(ngModule) {
ngModule.controller('GroupDataSourcesCtrl', GroupDataSourcesCtrl);
return {

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