mirror of
https://github.com/getredash/redash.git
synced 2025-12-19 17:37:19 -05:00
Compare commits
486 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
900d558857 | ||
|
|
c6dc9affed | ||
|
|
96486b5c58 | ||
|
|
7c1565017f | ||
|
|
7197370ad4 | ||
|
|
1cbf09cbbe | ||
|
|
28b4450fa9 | ||
|
|
a799303f53 | ||
|
|
59d6eb662c | ||
|
|
4e4a3e13ab | ||
|
|
095d07bcb8 | ||
|
|
71a235c79b | ||
|
|
2bc3885977 | ||
|
|
97217f56c1 | ||
|
|
ba36f7395d | ||
|
|
ea7ca9e632 | ||
|
|
5e5fc736bf | ||
|
|
f38e76ad10 | ||
|
|
80a6f357e3 | ||
|
|
bd91288d1a | ||
|
|
38389a28ed | ||
|
|
9ef9f29213 | ||
|
|
a3c2082b7f | ||
|
|
bc5516e941 | ||
|
|
65ac8c715e | ||
|
|
9874361466 | ||
|
|
b28c8fa227 | ||
|
|
048bd53eac | ||
|
|
95c707d028 | ||
|
|
41ec4c857b | ||
|
|
e62acb1d99 | ||
|
|
a9dc00aaa6 | ||
|
|
38c6152aa0 | ||
|
|
fb723328d4 | ||
|
|
047475562d | ||
|
|
acd33ec852 | ||
|
|
340a23e71c | ||
|
|
3db1b7f265 | ||
|
|
845357fa02 | ||
|
|
f75e31fa8e | ||
|
|
38be723179 | ||
|
|
18bf44453d | ||
|
|
374f11252f | ||
|
|
2d3566abce | ||
|
|
17d6bfff63 | ||
|
|
73540175d8 | ||
|
|
8c693efb3e | ||
|
|
51392d0398 | ||
|
|
78888c2082 | ||
|
|
bc6bd1b316 | ||
|
|
4060344a72 | ||
|
|
6522325060 | ||
|
|
ae6564e912 | ||
|
|
2af70a6c2d | ||
|
|
a3a1dcf4ba | ||
|
|
eb979ef130 | ||
|
|
7f7fdbba54 | ||
|
|
fa213d72a7 | ||
|
|
d2bf935edb | ||
|
|
c4349f5c64 | ||
|
|
b5a6f4a166 | ||
|
|
79807dfa14 | ||
|
|
0b0ec90987 | ||
|
|
a9fc220ec8 | ||
|
|
ee9bbbaa7c | ||
|
|
12cc4e5ff9 | ||
|
|
b5b5643090 | ||
|
|
6718081a49 | ||
|
|
138087861c | ||
|
|
9a88cf1743 | ||
|
|
2ca93599ef | ||
|
|
ef85a06d60 | ||
|
|
f7ffc75ba4 | ||
|
|
f28eda4174 | ||
|
|
c5458af1a0 | ||
|
|
c28ced14c6 | ||
|
|
1110e17c4a | ||
|
|
3b9c31a056 | ||
|
|
38b655ce3a | ||
|
|
0ec9b73eb2 | ||
|
|
b67369daa4 | ||
|
|
cbc7eee592 | ||
|
|
d512cef5af | ||
|
|
c6d1fc103c | ||
|
|
bf5b31b252 | ||
|
|
0c404fa602 | ||
|
|
0ebb6ada3c | ||
|
|
d2e519cc3b | ||
|
|
9b38f1e81c | ||
|
|
f03c173c57 | ||
|
|
f89842801f | ||
|
|
56d4ad74a8 | ||
|
|
334e95afa0 | ||
|
|
0443d84848 | ||
|
|
d38f251688 | ||
|
|
890243eb20 | ||
|
|
9fed3266e6 | ||
|
|
8fb665be08 | ||
|
|
c19253648e | ||
|
|
b8d2df7567 | ||
|
|
4603152930 | ||
|
|
e33e90a69d | ||
|
|
f5dcb5d58d | ||
|
|
f2f6abe775 | ||
|
|
c33189a355 | ||
|
|
781d997e76 | ||
|
|
35e02d8043 | ||
|
|
720af7dabf | ||
|
|
487a8c798c | ||
|
|
0f580f4540 | ||
|
|
cb21024e5c | ||
|
|
df7b970ff7 | ||
|
|
ff4edb4fbd | ||
|
|
131c9ef036 | ||
|
|
a3071a3ba1 | ||
|
|
8d5ce85954 | ||
|
|
9d3ae2c34a | ||
|
|
6d2337b332 | ||
|
|
1ef2238d65 | ||
|
|
521d05279b | ||
|
|
01e85f218a | ||
|
|
8af028bc90 | ||
|
|
85da5fced1 | ||
|
|
038d3b1004 | ||
|
|
6cf2b94a10 | ||
|
|
c930c44e3a | ||
|
|
0753332ef8 | ||
|
|
ed9e409e17 | ||
|
|
c40fffa107 | ||
|
|
d597665a86 | ||
|
|
b0bec26138 | ||
|
|
0d44466967 | ||
|
|
f4cb62782a | ||
|
|
3cadd6731c | ||
|
|
fc18b84f69 | ||
|
|
f7fc679427 | ||
|
|
e674b715ef | ||
|
|
029f6335ed | ||
|
|
fb4153add7 | ||
|
|
ada8a1255b | ||
|
|
505f338da9 | ||
|
|
18d9b2eec9 | ||
|
|
41a03352b9 | ||
|
|
50f817e265 | ||
|
|
04ddb289ee | ||
|
|
0152250e14 | ||
|
|
f574cdd179 | ||
|
|
458f213ea7 | ||
|
|
f2caae6eb1 | ||
|
|
c01cd89de9 | ||
|
|
5ea3ed7308 | ||
|
|
50eb9a86c9 | ||
|
|
12cbfc5d12 | ||
|
|
ba7ed5c6f0 | ||
|
|
4fbfa682fe | ||
|
|
fb1139a2ea | ||
|
|
8d8ec1a5f8 | ||
|
|
7582b3174d | ||
|
|
154b554ecd | ||
|
|
316e014cfa | ||
|
|
048d8fcb5b | ||
|
|
8bbb1cdfd4 | ||
|
|
94175b8a52 | ||
|
|
c350b43a5a | ||
|
|
b379c13e8b | ||
|
|
7d91e9d173 | ||
|
|
1b15ea8af9 | ||
|
|
e76efc9cdf | ||
|
|
0a311bf63f | ||
|
|
5069edb9b1 | ||
|
|
90162b6331 | ||
|
|
398812a14f | ||
|
|
2e44872b49 | ||
|
|
e02fdb3e37 | ||
|
|
234edd339c | ||
|
|
e5cbdf3036 | ||
|
|
9b85890204 | ||
|
|
6295e88d43 | ||
|
|
7796a57d43 | ||
|
|
df7fd13bfd | ||
|
|
6a5a843478 | ||
|
|
7d4fb280ba | ||
|
|
2a22b98c77 | ||
|
|
6b56e4a3e3 | ||
|
|
47fc6612bf | ||
|
|
f3e5c22c07 | ||
|
|
b42d2c5784 | ||
|
|
478a86a892 | ||
|
|
9e0205d148 | ||
|
|
59b7961bcd | ||
|
|
5b54a777d9 | ||
|
|
3af9b333a8 | ||
|
|
dcaecdbe16 | ||
|
|
3aa7d86699 | ||
|
|
feab2a7e7b | ||
|
|
d18220c1af | ||
|
|
8074a91b29 | ||
|
|
72560d985f | ||
|
|
ff2c8524de | ||
|
|
1bdea11fe3 | ||
|
|
a7bed64707 | ||
|
|
dc969fe0b5 | ||
|
|
588c868060 | ||
|
|
89de5f2a18 | ||
|
|
fe32877864 | ||
|
|
e739f90405 | ||
|
|
a07135c638 | ||
|
|
6b531ac568 | ||
|
|
5bce695fcc | ||
|
|
ba910280a1 | ||
|
|
2f386781d7 | ||
|
|
4c70349ee1 | ||
|
|
74b9c51dea | ||
|
|
d95c22fa24 | ||
|
|
3a7611309c | ||
|
|
5281d6c281 | ||
|
|
5bc1e71143 | ||
|
|
31ebfb80d7 | ||
|
|
1f8ed8a6c2 | ||
|
|
3d10718650 | ||
|
|
4a4ee49187 | ||
|
|
7bf7b00633 | ||
|
|
68e3fe65ba | ||
|
|
7fe096fba8 | ||
|
|
f80951457d | ||
|
|
1da165edc1 | ||
|
|
61c7c556b6 | ||
|
|
6170c48ed2 | ||
|
|
974f69aecf | ||
|
|
1a8078ab03 | ||
|
|
1bc8d586c3 | ||
|
|
a795f1463b | ||
|
|
aae77a8b25 | ||
|
|
c278209883 | ||
|
|
7f8ef2a050 | ||
|
|
d21e11ba33 | ||
|
|
1fc990f11a | ||
|
|
a09a767641 | ||
|
|
12ef64f10d | ||
|
|
776e52a77c | ||
|
|
6d8880c10d | ||
|
|
5d5af369e6 | ||
|
|
1d7fef4f7d | ||
|
|
35cb0bc805 | ||
|
|
5310385f15 | ||
|
|
41c791ff42 | ||
|
|
9b3f910326 | ||
|
|
aacc4b7b46 | ||
|
|
605a70d554 | ||
|
|
73466dc0e0 | ||
|
|
3fd90c6289 | ||
|
|
97624a3e2c | ||
|
|
d69c9409dd | ||
|
|
5bb5f46c02 | ||
|
|
2b6fe22b3f | ||
|
|
53f0716aca | ||
|
|
fd798ddcf5 | ||
|
|
812177a4e0 | ||
|
|
b9e08897ac | ||
|
|
e445fa436e | ||
|
|
797a0a30ca | ||
|
|
dfd16f3d7a | ||
|
|
a80aae0ec7 | ||
|
|
c8ad866a53 | ||
|
|
fe2f08cfd6 | ||
|
|
f64769cc80 | ||
|
|
831dfe6c8d | ||
|
|
3b4da81ec6 | ||
|
|
248c540543 | ||
|
|
0fb0ba6473 | ||
|
|
019a09945e | ||
|
|
520a5f8fa4 | ||
|
|
f840681377 | ||
|
|
300421792c | ||
|
|
0ab25c317c | ||
|
|
c8adf322a9 | ||
|
|
fae1e7152a | ||
|
|
e543e0c466 | ||
|
|
d61002a544 | ||
|
|
92f93f8ff6 | ||
|
|
bf17bdc32d | ||
|
|
70292c888c | ||
|
|
69cb5b72e1 | ||
|
|
470d2ad359 | ||
|
|
e85fa2a42c | ||
|
|
42116abcb3 | ||
|
|
2e0b930192 | ||
|
|
24ba110965 | ||
|
|
e1eeb67025 | ||
|
|
71c9cbd5a4 | ||
|
|
e1ac5bb038 | ||
|
|
c2e84c92c6 | ||
|
|
090962d09c | ||
|
|
df945a12b0 | ||
|
|
3f99f0c6d5 | ||
|
|
4706bebde0 | ||
|
|
85f729260b | ||
|
|
8bf2c15db8 | ||
|
|
9ea4784f87 | ||
|
|
8be9613640 | ||
|
|
b611c98112 | ||
|
|
ad3dbad8ac | ||
|
|
62c8bd3531 | ||
|
|
c8d66b3335 | ||
|
|
0217d419d1 | ||
|
|
45f448e0d1 | ||
|
|
ee4b05eb98 | ||
|
|
202c53c7d7 | ||
|
|
9816403c45 | ||
|
|
b6a1178499 | ||
|
|
2fe6110e0f | ||
|
|
c4e18bb481 | ||
|
|
add8f0eeeb | ||
|
|
694d971df9 | ||
|
|
36c93ce212 | ||
|
|
35fe1f23e3 | ||
|
|
2517abb27f | ||
|
|
1d749a83e1 | ||
|
|
fc50a7b9bb | ||
|
|
6f72d456d2 | ||
|
|
1182f8c6b0 | ||
|
|
f090f947b7 | ||
|
|
a8246471f4 | ||
|
|
229c33939c | ||
|
|
ef2eaf1fa9 | ||
|
|
d30f4f155a | ||
|
|
b2e5df6af2 | ||
|
|
0470cd6592 | ||
|
|
a517dad456 | ||
|
|
789ef1614d | ||
|
|
3dfab5009c | ||
|
|
7d5d7c4a6b | ||
|
|
5056d2fa90 | ||
|
|
1fad874dee | ||
|
|
a2c79367de | ||
|
|
bcf129e646 | ||
|
|
94077ccafd | ||
|
|
411ef7bd00 | ||
|
|
6b22c2c541 | ||
|
|
e385a147f6 | ||
|
|
ad69a6be3f | ||
|
|
a6c45da2ca | ||
|
|
d5c4d9336f | ||
|
|
c1f8e2a4e0 | ||
|
|
fe42195b5a | ||
|
|
6a0bb82f3c | ||
|
|
69825e001f | ||
|
|
ad8571f2e3 | ||
|
|
fbd3b92ba0 | ||
|
|
4f6c433f1b | ||
|
|
412f469035 | ||
|
|
eee38557d1 | ||
|
|
23cb92cf6d | ||
|
|
108137bd7e | ||
|
|
6bc53c3638 | ||
|
|
e54fff402e | ||
|
|
8d125354d2 | ||
|
|
fc96e14a8f | ||
|
|
178dfa59c1 | ||
|
|
8719de7120 | ||
|
|
af8bdf4fd1 | ||
|
|
764e347b74 | ||
|
|
2f1b1a69bd | ||
|
|
b2fea428dd | ||
|
|
1f1d7996ec | ||
|
|
47dc9a136f | ||
|
|
6ed86d9ce5 | ||
|
|
8e760705a6 | ||
|
|
9c606b9660 | ||
|
|
f65b3223f4 | ||
|
|
e85e962466 | ||
|
|
a7df809c4d | ||
|
|
beb29c66c2 | ||
|
|
749171b186 | ||
|
|
40a8187b1e | ||
|
|
6b7234c910 | ||
|
|
3807510bfe | ||
|
|
3650617928 | ||
|
|
d60843fa5b | ||
|
|
5a5917a04a | ||
|
|
ae642fddf7 | ||
|
|
b4a8fb76de | ||
|
|
b885ccb09c | ||
|
|
b70c329307 | ||
|
|
1aa54543ed | ||
|
|
e050c085df | ||
|
|
62962d28ca | ||
|
|
d7c502eb50 | ||
|
|
dd7841dc15 | ||
|
|
14c751b39e | ||
|
|
f4297ff3b0 | ||
|
|
79ffbbbe4b | ||
|
|
4c1cb037a0 | ||
|
|
f679dc7562 | ||
|
|
76470b9f09 | ||
|
|
3edec570f1 | ||
|
|
a2e07b46f2 | ||
|
|
326a80895c | ||
|
|
d200cc7405 | ||
|
|
c4dff40e1d | ||
|
|
21636c4d65 | ||
|
|
52084c322f | ||
|
|
5fd2dadef4 | ||
|
|
f312e89323 | ||
|
|
0046cfa3ee | ||
|
|
4ecc8da398 | ||
|
|
382431e34b | ||
|
|
6023dc5f3d | ||
|
|
2d38b38a7d | ||
|
|
3513d84bb8 | ||
|
|
24cd55f5cc | ||
|
|
0bce6996bf | ||
|
|
6edfdfba63 | ||
|
|
ddbbe1267a | ||
|
|
7fada5d5f7 | ||
|
|
c3f5a37e21 | ||
|
|
a76c87b3ae | ||
|
|
4a0612328e | ||
|
|
931c322ad7 | ||
|
|
58c61641d3 | ||
|
|
b675cd19d7 | ||
|
|
17b9f976c8 | ||
|
|
248808e165 | ||
|
|
7c6327be57 | ||
|
|
a86ece66b5 | ||
|
|
fd9461ef20 | ||
|
|
f121c609ad | ||
|
|
24f3e071e3 | ||
|
|
914977f279 | ||
|
|
97b92d8887 | ||
|
|
c1981b17a4 | ||
|
|
ea7c6c2be3 | ||
|
|
22e3a4d8f2 | ||
|
|
75ebbe148b | ||
|
|
75f90c190b | ||
|
|
8aa053ce21 | ||
|
|
23ba8b4aa1 | ||
|
|
6dde3170ab | ||
|
|
b9144a9d7a | ||
|
|
a1a0d766fe | ||
|
|
48322856d9 | ||
|
|
b9f8b6cdbf | ||
|
|
805ea3cb46 | ||
|
|
79187cd29a | ||
|
|
ccaf78767b | ||
|
|
94a14f93a8 | ||
|
|
5ba6af6ad4 | ||
|
|
25760494d7 | ||
|
|
a1fbd511a9 | ||
|
|
1b756de479 | ||
|
|
a9e53a6c29 | ||
|
|
111fbfd483 | ||
|
|
437778a8be | ||
|
|
1c955a570d | ||
|
|
469b041a2f | ||
|
|
7a47d6741d | ||
|
|
214a231371 | ||
|
|
eb3e30f70f | ||
|
|
7324f1f4c7 | ||
|
|
93df24de39 | ||
|
|
9f21807647 | ||
|
|
9b59394768 | ||
|
|
ac1b0a46f9 | ||
|
|
5d7795ca47 | ||
|
|
519fb49f6a | ||
|
|
f852f935c5 | ||
|
|
ccf9cbd2c8 | ||
|
|
730b7c8cad | ||
|
|
ac557fd5b5 | ||
|
|
91396f0c52 | ||
|
|
9f3fd021ab | ||
|
|
9dedaa31c5 | ||
|
|
de77ebd961 | ||
|
|
7bdc42ff05 | ||
|
|
5306814237 | ||
|
|
7939e04e74 | ||
|
|
f77da51a7d | ||
|
|
ebaf012701 | ||
|
|
8c481cd7a7 | ||
|
|
081ac5f651 | ||
|
|
ac538c35e9 | ||
|
|
eeee592abc | ||
|
|
ce65578c72 | ||
|
|
f4c25cb941 | ||
|
|
ba0daa218e | ||
|
|
fefcb928da |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -9,14 +9,8 @@ celerybeat-schedule*
|
||||
\#*#
|
||||
*~
|
||||
_build
|
||||
|
||||
# Vagrant related
|
||||
.vagrant
|
||||
Berksfile.lock
|
||||
redash/dump.rdb
|
||||
.vscode
|
||||
.env
|
||||
.ruby-version
|
||||
venv
|
||||
|
||||
dump.rdb
|
||||
|
||||
|
||||
503
CHANGELOG.md
503
CHANGELOG.md
@@ -1,5 +1,186 @@
|
||||
# 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
|
||||
|
||||
- [Cassandra] Support for UUID serializing and setting protocol version. @mfouilleul
|
||||
- [BigQuery] Add maximumBillingTier to BigQuery configuration. @dotneet
|
||||
- Add the propertyOrder field to specify order of data source settings. @rmakulov
|
||||
- Add Plotly based Boxplot visualization. @deecay
|
||||
- [Presto] Add: query cancellation support. @fbertsch
|
||||
- [MongoDB] add $oids JSON extension.
|
||||
- [PostgreSQL] support for loading materialized views in schema.
|
||||
- [MySQL] Add option to hide SSL settings.
|
||||
- [MySQL] support for RDS MySQL and SSL.
|
||||
- [Google Analytics] support for mcf queries & better errors.
|
||||
- Add: static enum parameter type. @rockwotj
|
||||
- Add: option to hide pivot table controls. @deecay
|
||||
- Retry reload of query results if it had an error.
|
||||
- [Data Sources] Add: MemSQL query runner. @alexanderlz
|
||||
- "Dumb" recents option (see #1779 for details)
|
||||
- Athena: direct query runner using the instead of JDBC proxy. @laughingman7743
|
||||
- Optionally support parameters in embeds. @ziahamza
|
||||
- Sorting ability in alerts view.
|
||||
- Option to change default encoding of CSV writer. @yamamanx
|
||||
- Ability to set dashboard level filters from UI.
|
||||
- CLI command to open IPython shell.
|
||||
- Add link to query page from admin view. @miketheman
|
||||
- Add the option to write logs to STDOUT instead of STDERR. @eyalzek
|
||||
- Add limit parameter to tasks API. @alexpekurovsky
|
||||
- Add SQLAlchemy pool settings.
|
||||
- Support for category type y axis.
|
||||
- Add 12 & 24 hours refresh rate option to dashboards.
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgrade Google API client library for all Google data sources. @ahamino
|
||||
- [JIRA JQL] change default max results limit from 50 to 1000. @jvanegmond
|
||||
- Upgrade to newer Plotly version. @deecay
|
||||
- [Athena] Configuration flag to disable query annotations for Athena. @suemoc
|
||||
- Ignore extra columns in CSV output. @alexanderlz
|
||||
- [TreasureData] improve error handling and upgrade client.
|
||||
- [InfluxDB] simpler test connection query (show databases requires admin).
|
||||
- [MSSQL] Mark integers as decimals as well, as sometimes decimal columns being returned
|
||||
with integer column type.
|
||||
- [Google Spreadsheets] add timeout to requests.
|
||||
- Sort dashboards list by name. @deecay
|
||||
- Include Celery task name in statsd metrics.
|
||||
- Don't include paused datasource's queries in outdated queries count.
|
||||
- Cohort: handle the case where the value/total might be strings.
|
||||
- Query results: better type guessing on the client side.
|
||||
- Counter: support negative indexes to iterate from the end of the results.
|
||||
- Data sources and destinations configuration: change order of name and type (type first now).
|
||||
- Show API Key in a modal dialog instead of alert.
|
||||
- Sentry: upgrade client version.
|
||||
- Sentry: don't install logging hook.
|
||||
- Split refresh schemas into separate tasks and add a timeout.
|
||||
- Execute scheduled queries with parameters using their default value.
|
||||
- Keep track of last query execution (including failed ones) for scheduling purposes.
|
||||
- Same view for input on search result page as in header. @44px
|
||||
- Metrics: report endpoints without dots for metrics.
|
||||
- Redirect to / when org not found.
|
||||
- Improve parameters label placement. @44px
|
||||
- Auto-publish queries when they are named (with option to disable; #1830).
|
||||
- Show friendly error message in case of duplicate data source name.
|
||||
- Don't allow saving dashboard with empty name.
|
||||
- Enable strict checking for Angular DI.
|
||||
- Disable Angular debug info (should improve performance).
|
||||
- Update to Webpack 2. @44px
|
||||
- Remove /forgot endpoint if REDASH_PASSWORD_LOGIN_ENABLED is false. @amarjayr
|
||||
- Docker: make Gunicorn worker count configurable. @unixwitch
|
||||
- Snowflake support is no longer enabled by default.
|
||||
- Enable memory optimization for Excel exporter.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix: set default values in options to enable 'default: True' for checkbox. @rmakulov
|
||||
- Support MULTI_ORG again.
|
||||
- [Google Spreadsheets] handle distant future dates.
|
||||
- [SQLite] better handle utf-8 error messages.
|
||||
- Fix: don't remove locks for queries with task status of PENDING.
|
||||
- Only split columns with __/:: that end with filter/MultiFilter.
|
||||
- Alert notifications fail (sometime) with a SQLAlchemy error.
|
||||
- Safeguard against empty query results when checking alert status. @danielerapati
|
||||
- Delete data source doesn't work when query results referenced by queries.
|
||||
- Fix redirect to /setup on the last setup step. @44px
|
||||
- Cassandra: use port setting in connection options. @yershalom
|
||||
- Metrics: table name wasn't found for count queries.
|
||||
- BigQuery wasn't loading due to bad import.
|
||||
- DynamicForm component was inserting empty values.
|
||||
- Clear null values from data source options dictionary.
|
||||
- /api/session API call wasn't working when multi tenancy enabled
|
||||
- If column had no type it would use previous column's type.
|
||||
- Alert destination details were not updating.
|
||||
- When setting rearm on a new alert, it wasn't persisted.
|
||||
- Salesforce: sandbox parameter should be optional. @msnider
|
||||
- Alert page wasn't properly linked from alerts list. @alison985
|
||||
- PostgreSQL passwords with spaces were not supported. (#1056)
|
||||
- PivotTable wasn't updating after first save.
|
||||
|
||||
|
||||
## v1.0.3 - 2017-04-18
|
||||
|
||||
### Fixed
|
||||
@@ -54,8 +235,11 @@
|
||||
- Fix: page header wasn't updating on dashboards page @MichaelJAndy
|
||||
- Fix: keyboard shortcuts didn't work in parameter inputs
|
||||
|
||||
## v1.0.0-rc.2 - 2017-02-22
|
||||
### Other
|
||||
|
||||
- Change default job expiry times to: job lock expire after 12 hours (previously: 6 hours) and Celery task result object expire after 4 hours (previously: 1 hour). @shimpeko
|
||||
|
||||
## v1.0.0-rc.2 - 2017-02-22
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -100,7 +284,7 @@
|
||||
## v1.0.0-rc.1 - 2017-01-31
|
||||
|
||||
This version has two big changes behind the scenes:
|
||||
|
||||
|
||||
* Refactor the frontend to use latest (at the time) Angular version (1.5) along with better frontend pipeline based on
|
||||
WebPack.
|
||||
* Refactor the backend code to use SQLAlchemy and Alembic, for easier migrations/upgrades.
|
||||
@@ -186,109 +370,110 @@ We're releasing a new upgrade script -- see [here](https://redash.io/help-onprem
|
||||
|
||||
### Added
|
||||
|
||||
61fe16e #1374: Add: allow '*' in REDASH_CORS_ACCESS_CONTROL_ALLOW_ORIGIN (Allen Short)
|
||||
2f09043 #1113: Add: share modify/access permissions for queries and dashboard (whummer)
|
||||
3db0eea #1341: Add: support for specifying SAML nameid-format (zoetrope)
|
||||
b0ecd0e #1343: Add: support for local SAML metadata file (zoetrope)
|
||||
0235d37 #1335: Add: allow changing alert email subject. (Arik Fraimovich)
|
||||
2135dfd #1333: Add: control over y axis min/max values (Arik Fraimovich)
|
||||
49e788a #1328: Add: support for snapshot generation service (Arik Fraimovich)
|
||||
229ca6c #1323: Add: collect runtime metrics for Celery tasks (Arik Fraimovich)
|
||||
931a1f3 #1315: Add: support for loading BigQuery schema (Arik Fraimovich)
|
||||
39b4f9a #1314: Add: support MongoDB SSL connections (Arik Fraimovich)
|
||||
ca1ca9b #1312: Add: additional configuration for Celery jobs (Arik Fraimovich)
|
||||
fc00e61 #1310: Add: support for date/time with seconds parameters (Arik Fraimovich)
|
||||
d72a198 #1307: Add: API to force refresh data source schema (Arik Fraimovich)
|
||||
beb89ec #1305: Add: UI to edit dashboard text box widget (Kazuhito Hokamura)
|
||||
808fdd4 #1298: Add: JIRA (JQL) query runner (Arik Fraimovich)
|
||||
ff9e844 #1280: Add: configuration flag to disable scheduled queries (Hirotaka Suzuki)
|
||||
ef4699a #1269: Add: Google Drive federated tables support in BigQuery query runner (Kurt Gooden)
|
||||
2eeb947 #1236: Add: query runner for Cassandra and ScyllaDB (syerushalmy)
|
||||
10b398e #1249: Add: override slack webhook parameters (mystelynx)
|
||||
2b5e340 #1252: Add: Schema loading support for Presto query runner (using information_schema) (Rohan Dhupelia)
|
||||
2aaf5dd #1250: Add: query snippets feature (Arik Fraimovich)
|
||||
8d8af73 #1226: Add: Sankey visualization (Arik Fraimovich)
|
||||
a02edda #1222: Add: additional results format for sunburst visualization (Arik Fraimovich)
|
||||
0e70188 #1213: Add: new sunburst sequence visualization (Arik Fraimovich)
|
||||
9a6d2d7 #1204: Add: show views in schema browser for Vertica data sources (Matthew Carter)
|
||||
600afa5 #1138: Add: ability to register user defined function (UDF) resources for BigQuery DataSource/Query (fabito)
|
||||
b410410 #1166: Add: "every 14 days" refresh option (Arik Fraimovich)
|
||||
906365f #967: Add: extend ElasticSearch query_runner to support aggregations (lloydw)
|
||||
- 61fe16e #1374: Add: allow '*' in REDASH_CORS_ACCESS_CONTROL_ALLOW_ORIGIN (Allen Short)
|
||||
- 2f09043 #1113: Add: share modify/access permissions for queries and dashboard (whummer)
|
||||
- 3db0eea #1341: Add: support for specifying SAML nameid-format (zoetrope)
|
||||
- b0ecd0e #1343: Add: support for local SAML metadata file (zoetrope)
|
||||
- 0235d37 #1335: Add: allow changing alert email subject. (Arik Fraimovich)
|
||||
- 2135dfd #1333: Add: control over y axis min/max values (Arik Fraimovich)
|
||||
- 49e788a #1328: Add: support for snapshot generation service (Arik Fraimovich)
|
||||
- 229ca6c #1323: Add: collect runtime metrics for Celery tasks (Arik Fraimovich)
|
||||
- 931a1f3 #1315: Add: support for loading BigQuery schema (Arik Fraimovich)
|
||||
- 39b4f9a #1314: Add: support MongoDB SSL connections (Arik Fraimovich)
|
||||
- ca1ca9b #1312: Add: additional configuration for Celery jobs (Arik Fraimovich)
|
||||
- fc00e61 #1310: Add: support for date/time with seconds parameters (Arik Fraimovich)
|
||||
- d72a198 #1307: Add: API to force refresh data source schema (Arik Fraimovich)
|
||||
- beb89ec #1305: Add: UI to edit dashboard text box widget (Kazuhito Hokamura)
|
||||
- 808fdd4 #1298: Add: JIRA (JQL) query runner (Arik Fraimovich)
|
||||
- ff9e844 #1280: Add: configuration flag to disable scheduled queries (Hirotaka Suzuki)
|
||||
- ef4699a #1269: Add: Google Drive federated tables support in BigQuery query runner (Kurt Gooden)
|
||||
- 2eeb947 #1236: Add: query runner for Cassandra and ScyllaDB (syerushalmy)
|
||||
- 10b398e #1249: Add: override slack webhook parameters (mystelynx)
|
||||
- 2b5e340 #1252: Add: Schema loading support for Presto query runner (using information_schema) (Rohan Dhupelia)
|
||||
- 2aaf5dd #1250: Add: query snippets feature (Arik Fraimovich)
|
||||
- 8d8af73 #1226: Add: Sankey visualization (Arik Fraimovich)
|
||||
- a02edda #1222: Add: additional results format for sunburst visualization (Arik Fraimovich)
|
||||
- 0e70188 #1213: Add: new sunburst sequence visualization (Arik Fraimovich)
|
||||
- 9a6d2d7 #1204: Add: show views in schema browser for Vertica data sources (Matthew Carter)
|
||||
- 600afa5 #1138: Add: ability to register user defined function (UDF) resources for BigQuery DataSource/Query (fabito)
|
||||
- b410410 #1166: Add: "every 14 days" refresh option (Arik Fraimovich)
|
||||
- 906365f #967: Add: extend ElasticSearch query_runner to support aggregations (lloydw)
|
||||
|
||||
### Changed
|
||||
|
||||
2de4aa2 #1395: Change: switch to requests in URL query runner (Arik Fraimovich)
|
||||
db1a941 #1392: Change: Update documentation links to point at the new location. (Arik Fraimovich)
|
||||
002f794 #1368: Change: added ability to disable auto update in admin views (Arik Fraimovich)
|
||||
aa5d14e #1366: Change: improve error message for exception in the Python query runner (deecay)
|
||||
880627c #1355: Change: pass the user object to the run_query method (Arik Fraimovich)
|
||||
23c605b #1342: SAML: specify entity id (zoetrope)
|
||||
015b1dc #1334: Change: allow specifying recipient address when sending email test message (Arik Fraimovich)
|
||||
39aaa2f #1292: Change: improvements to map visualization (Arik Fraimovich)
|
||||
b22191b #1332: Change: upgrade Python packages (Arik Fraimovich)
|
||||
23ba98b #1331: Celery: Upgrade Celery to more recent version. (Arik Fraimovich)
|
||||
3283116 #1330: Change: upgrade Requests to latest version. (Arik Fraimovich)
|
||||
39091e0 #1324: Change: add more logging and information for refresh schemas task (Arik Fraimovich)
|
||||
462faea #1316: Change: remove deprecated settings (Arik Fraimovich)
|
||||
73e1837 #1313: Change: more flexible column width calculation (Arik Fraimovich)
|
||||
e8eb840 #1279: Change: update bootstrap.sh to support Ubuntu 16.04 (IllusiveMilkman)
|
||||
8cf0252 #1262: Change: upgrade Plot.ly version and switch to smaller build (Arik Fraimovich)
|
||||
0b79fb8 #1306: Change: paginate queries page & add explicit urls. (Arik Fraimovich)
|
||||
41f99f5 #1299: Change: send Content-Type header (application/json) in query results responses (Tsuyoshi Tatsukawa)
|
||||
dfb1a20 #1297: Change: update Slack configuration titles. (Arik Fraimovich)
|
||||
8c1056c #1294: Change: don't annotate BigQuery queries (Arik Fraimovich)
|
||||
a3cf92e #1289: Change: use key_as_string when available (ElasticSearch query runner) (Arik Fraimovich)
|
||||
e155191 #1285: Change: do not display Oracle tablespace name in schema browser (Matthew Carter)
|
||||
6cbc39c #1282: Change: deduplicate Google Spreadsheet columns (Arik Fraimovich)
|
||||
4caf2e3 #1277: Set specific version of cryptography lib (Arik Fraimovich)
|
||||
d22f0d4 #1216: Change: bootstrap.sh - use non interactive dist-upgrade (Atsushi Sasaki)
|
||||
19530f4 #1245: Change: switch from CodeMirror to Ace editor (Arik Fraimovich)
|
||||
dfb92db #1234: Change: MongoDB query runner set DB name as mandatory (Arik Fraimovich)
|
||||
b750843 #1230: Change: annotate Presto queries with metadata (Noriaki Katayama)
|
||||
5b20fe2 #1217: Change: install libffi-dev for Cryptography (Ubuntu setup script) (Atsushi Sasaki)
|
||||
a9fac34 #1206: Change: update pymssql version to 2.1.3 (kitsuyui)
|
||||
5d43cbe #1198: Change: add support for Standard SQL in BigQuery query runner (mystelynx)
|
||||
84d0c22 #1193: Change: modify the argument order of moment.add function call (Kenya Yamaguchi)
|
||||
- 2de4aa2 #1395: Change: switch to requests in URL query runner (Arik Fraimovich)
|
||||
- db1a941 #1392: Change: Update documentation links to point at the new location. (Arik Fraimovich)
|
||||
- 002f794 #1368: Change: added ability to disable auto update in admin views (Arik Fraimovich)
|
||||
- aa5d14e #1366: Change: improve error message for exception in the Python query runner (deecay)
|
||||
- 880627c #1355: Change: pass the user object to the run_query method (Arik Fraimovich)
|
||||
- 23c605b #1342: SAML: specify entity id (zoetrope)
|
||||
- 015b1dc #1334: Change: allow specifying recipient address when sending email test message (Arik Fraimovich)
|
||||
- 39aaa2f #1292: Change: improvements to map visualization (Arik Fraimovich)
|
||||
- b22191b #1332: Change: upgrade Python packages (Arik Fraimovich)
|
||||
- 23ba98b #1331: Celery: Upgrade Celery to more recent version. (Arik Fraimovich)
|
||||
- 3283116 #1330: Change: upgrade Requests to latest version. (Arik Fraimovich)
|
||||
- 39091e0 #1324: Change: add more logging and information for refresh schemas task (Arik Fraimovich)
|
||||
- 462faea #1316: Change: remove deprecated settings (Arik Fraimovich)
|
||||
- 73e1837 #1313: Change: more flexible column width calculation (Arik Fraimovich)
|
||||
- e8eb840 #1279: Change: update bootstrap.sh to support Ubuntu 16.04 (IllusiveMilkman)
|
||||
- 8cf0252 #1262: Change: upgrade Plot.ly version and switch to smaller build (Arik Fraimovich)
|
||||
- 0b79fb8 #1306: Change: paginate queries page & add explicit urls. (Arik Fraimovich)
|
||||
- 41f99f5 #1299: Change: send Content-Type header (application/json) in query results responses (Tsuyoshi Tatsukawa)
|
||||
- dfb1a20 #1297: Change: update Slack configuration titles. (Arik Fraimovich)
|
||||
- 8c1056c #1294: Change: don't annotate BigQuery queries (Arik Fraimovich)
|
||||
- a3cf92e #1289: Change: use key_as_string when available (ElasticSearch query runner) (Arik Fraimovich)
|
||||
- e155191 #1285: Change: do not display Oracle tablespace name in schema browser (Matthew Carter)
|
||||
- 6cbc39c #1282: Change: deduplicate Google Spreadsheet columns (Arik Fraimovich)
|
||||
- 4caf2e3 #1277: Set specific version of cryptography lib (Arik Fraimovich)
|
||||
- d22f0d4 #1216: Change: bootstrap.sh - use non interactive dist-upgrade (Atsushi Sasaki)
|
||||
- 19530f4 #1245: Change: switch from CodeMirror to Ace editor (Arik Fraimovich)
|
||||
- dfb92db #1234: Change: MongoDB query runner set DB name as mandatory (Arik Fraimovich)
|
||||
- b750843 #1230: Change: annotate Presto queries with metadata (Noriaki Katayama)
|
||||
- 5b20fe2 #1217: Change: install libffi-dev for Cryptography (Ubuntu setup script) (Atsushi Sasaki)
|
||||
- a9fac34 #1206: Change: update pymssql version to 2.1.3 (kitsuyui)
|
||||
- 5d43cbe #1198: Change: add support for Standard SQL in BigQuery query runner (mystelynx)
|
||||
- 84d0c22 #1193: Change: modify the argument order of moment.add function call (Kenya Yamaguchi)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
d6febb0 #1375: Fix: Download Dataset does not work when not logged in (Joshua Dechant)
|
||||
96553ad #1369: Fix: missing format call in Elasticsearch test method (Adam Griffiths)
|
||||
c57c765 #1365: Fix: compare retrieval times in UTC timezone (Allen Short)
|
||||
37dff5f #1360: Fix: connection test was broken for MySQL (ichihara)
|
||||
360028c #1359: Fix: schema loading query for Hive was wrong for non default schema (laughingman7743)
|
||||
7ee41d4 #1358: Fix: make sure all calls to run_query updated with new parameter (Arik Fraimovich)
|
||||
0d94479 #1329: Fix: Redis memory leak. (Arik Fraimovich)
|
||||
7145aa2 #1325: Fix: queries API was doing N+1 queries in most cases (Arik Fraimovich)
|
||||
cd2e927 #1311: Fix: BoxPlot visualization wasn't rendering on a dashboard (Arik Fraimovich)
|
||||
a562ce7 #1309: Fix: properly render checkboxes in dynamic forms (Arik Fraimovich)
|
||||
d48192c #1308: Fix: support for Unicode columns name in Google Spreadsheets (Arik Fraimovich)
|
||||
e42f93f #1283: Fix: schema browser was unstable after opening a table (Arik Fraimovich)
|
||||
170bd65 #1272: Fix: TreasureData get_schema method was returning array instead of string as column name (ariarijp)
|
||||
4710c41 #1265: Fix: refresh modal not working for unsaved query (Arik Fraimovich)
|
||||
bc3a5ab #1264: Fix: dashboard refresh not working (Arik Fraimovich)
|
||||
6202d09 #1240: Fix: when shared dashboard token not found, return 404 (Wesley Batista)
|
||||
93aac14 #1251: Fix: autocomplete went crazy when database has no autocomplete. (Arik Fraimovich)
|
||||
b8eca28 #1246: Fix: support large schemas in schema browser (Arik Fraimovich)
|
||||
b781003 #1223: Fix: Alert: when hipchat Alert.name is multibyte character, occur error. (toyama0919)
|
||||
0b928e6 #1227: Fix: Bower install fails in vagrant (Kazuhito Hokamura)
|
||||
a411af2 #1232: Fix: don't show warning when query string (parameters value) changes (Kazuhito Hokamura)
|
||||
3dbb5a6 #1221: Fix: sunburst didn't handle all cases of path lengths (Arik Fraimovich)
|
||||
a7cc1ee #1218: Fix: updated result not being saved when changing query text. (Arik Fraimovich)
|
||||
0617833 #1215: Fix: email alerts not working (Arik Fraimovich)
|
||||
78f65b1 #1187: Fix: read only users receive the permission error modal in query view (Arik Fraimovich)
|
||||
bba801f #1167: Fix the version of setuptools on bootstrap script for Ubuntu (Takuya Arita)
|
||||
ce81d69 #1160: Fix indentation in docker-compose-example.yml (Hirofumi Wakasugi)
|
||||
dd759fe #1155: Fix: make all configuration values of Oracle required (Arik Fraimovich)
|
||||
- d6febb0 #1375: Fix: Download Dataset does not work when not logged in (Joshua Dechant)
|
||||
- 96553ad #1369: Fix: missing format call in Elasticsearch test method (Adam Griffiths)
|
||||
- c57c765 #1365: Fix: compare retrieval times in UTC timezone (Allen Short)
|
||||
- 37dff5f #1360: Fix: connection test was broken for MySQL (ichihara)
|
||||
- 360028c #1359: Fix: schema loading query for Hive was wrong for non default schema (laughingman7743)
|
||||
- 7ee41d4 #1358: Fix: make sure all calls to run_query updated with new parameter (Arik Fraimovich)
|
||||
- 0d94479 #1329: Fix: Redis memory leak. (Arik Fraimovich)
|
||||
- 7145aa2 #1325: Fix: queries API was doing N+1 queries in most cases (Arik Fraimovich)
|
||||
- cd2e927 #1311: Fix: BoxPlot visualization wasn't rendering on a dashboard (Arik Fraimovich)
|
||||
- a562ce7 #1309: Fix: properly render checkboxes in dynamic forms (Arik Fraimovich)
|
||||
- d48192c #1308: Fix: support for Unicode columns name in Google Spreadsheets (Arik Fraimovich)
|
||||
- e42f93f #1283: Fix: schema browser was unstable after opening a table (Arik Fraimovich)
|
||||
- 170bd65 #1272: Fix: TreasureData get_schema method was returning array instead of string as column name (ariarijp)
|
||||
- 4710c41 #1265: Fix: refresh modal not working for unsaved query (Arik Fraimovich)
|
||||
- bc3a5ab #1264: Fix: dashboard refresh not working (Arik Fraimovich)
|
||||
- 6202d09 #1240: Fix: when shared dashboard token not found, return 404 (Wesley Batista)
|
||||
- 93aac14 #1251: Fix: autocomplete went crazy when database has no autocomplete. (Arik Fraimovich)
|
||||
- b8eca28 #1246: Fix: support large schemas in schema browser (Arik Fraimovich)
|
||||
- b781003 #1223: Fix: Alert: when hipchat Alert.name is multibyte character, occur error. (toyama0919)
|
||||
- 0b928e6 #1227: Fix: Bower install fails in vagrant (Kazuhito Hokamura)
|
||||
- a411af2 #1232: Fix: don't show warning when query string (parameters value) changes (Kazuhito Hokamura)
|
||||
- 3dbb5a6 #1221: Fix: sunburst didn't handle all cases of path lengths (Arik Fraimovich)
|
||||
- a7cc1ee #1218: Fix: updated result not being saved when changing query text. (Arik Fraimovich)
|
||||
- 0617833 #1215: Fix: email alerts not working (Arik Fraimovich)
|
||||
- 78f65b1 #1187: Fix: read only users receive the permission error modal in query view (Arik Fraimovich)
|
||||
- bba801f #1167: Fix the version of setuptools on bootstrap script for Ubuntu (Takuya Arita)
|
||||
- ce81d69 #1160: Fix indentation in docker-compose-example.yml (Hirofumi Wakasugi)
|
||||
- dd759fe #1155: Fix: make all configuration values of Oracle required (Arik Fraimovich)
|
||||
|
||||
### Docs
|
||||
a69ee0c #1225: Fix: RST formatting of the Vagrant documentation (Kazuhito Hokamura)
|
||||
03837c0 #1242: Docs: add warning re. quotes on column names and BigQuery (Ereli)
|
||||
9a98075 #1255: Docs: add documentation for InfluxDB (vishesh92)
|
||||
e0485de #1195: Docs: fix typo in maintenance page title (Antoine Augusti)
|
||||
7681d3e #1164: Docs: update permission documentation (Daniel Darabos)
|
||||
bcd3670 #1156: Docs: add SSL parameters to nginx configuration (Josh Cox)
|
||||
|
||||
- a69ee0c #1225: Fix: RST formatting of the Vagrant documentation (Kazuhito Hokamura)
|
||||
- 03837c0 #1242: Docs: add warning re. quotes on column names and BigQuery (Ereli)
|
||||
- 9a98075 #1255: Docs: add documentation for InfluxDB (vishesh92)
|
||||
- e0485de #1195: Docs: fix typo in maintenance page title (Antoine Augusti)
|
||||
- 7681d3e #1164: Docs: update permission documentation (Daniel Darabos)
|
||||
- bcd3670 #1156: Docs: add SSL parameters to nginx configuration (Josh Cox)
|
||||
|
||||
## v0.11.1.b2095 - 2016-08-02
|
||||
|
||||
@@ -306,73 +491,77 @@ Also, this release includes numerous smaller features, improvements, and bug fix
|
||||
A big thank you goes to all who contributed code and documentation in this release: @AntoineAugusti, @James226, @adamlwgriffiths, @alexdebrie, @anthony-coble, @ariarijp, @dheerajrav, @edwardsharp, @machira, @nabilblk, @ninneko, @ordd, @tomerben, @toru-takahashi, @vishesh92, @vorakumar and @whummer.
|
||||
|
||||
### Added
|
||||
d5e5b24 #1136: Feature: add --org option to all relevant CLI commands. (@adamlwgriffiths)
|
||||
87e25f2 #1129: Feature: support for JSON query formatting (Mongo, ElasticSearch) (@arikfr)
|
||||
6bb2716 #1121: Show error when failing to communicate with server (@arikfr)
|
||||
f21276e #1119: Feature: add UI to delete alerts (@arikfr)
|
||||
8656540 #1069: Feature: UI for query parameters (@arikfr)
|
||||
790128c #1067: Feature: word cloud visualization (@anthony-coble)
|
||||
8b73a2b #1098: Feature: UI for alert destinations & new destination types (@alexdebrie)
|
||||
1fbeb5d #1092: Add Heroku support (@adamlwgriffiths)
|
||||
f64622d #1089: Add support for serialising UUID type within MSSQL #961 (@James226)
|
||||
857caab #1085: Feature: API to pause a data source (@arikfr)
|
||||
214aa3b #1060: Feature: support configuring user's groups with SAML (@vorakumar)
|
||||
e20a005 #1007: Issue#1006: Make bottom margin editable for Chart visualization (@vorakumar)
|
||||
6e0dd2b #1063: Add support for date/time Y axis (@tomerben)
|
||||
b5a4a6b #979: Feature: Add CLI to edit group permissions (@ninneko)
|
||||
6d495d2 #1014: Add server-side parameter handling for embeds (@whummer)
|
||||
5255804 #1091: Add caching for queries used in embeds (@whummer)
|
||||
|
||||
- d5e5b24 #1136: Feature: add --org option to all relevant CLI commands. (@adamlwgriffiths)
|
||||
- 87e25f2 #1129: Feature: support for JSON query formatting (Mongo, ElasticSearch) (@arikfr)
|
||||
- 6bb2716 #1121: Show error when failing to communicate with server (@arikfr)
|
||||
- f21276e #1119: Feature: add UI to delete alerts (@arikfr)
|
||||
- 8656540 #1069: Feature: UI for query parameters (@arikfr)
|
||||
- 790128c #1067: Feature: word cloud visualization (@anthony-coble)
|
||||
- 8b73a2b #1098: Feature: UI for alert destinations & new destination types (@alexdebrie)
|
||||
- 1fbeb5d #1092: Add Heroku support (@adamlwgriffiths)
|
||||
- f64622d #1089: Add support for serialising UUID type within MSSQL #961 (@James226)
|
||||
- 857caab #1085: Feature: API to pause a data source (@arikfr)
|
||||
- 214aa3b #1060: Feature: support configuring user's groups with SAML (@vorakumar)
|
||||
- e20a005 #1007: Issue#1006: Make bottom margin editable for Chart visualization (@vorakumar)
|
||||
- 6e0dd2b #1063: Add support for date/time Y axis (@tomerben)
|
||||
- b5a4a6b #979: Feature: Add CLI to edit group permissions (@ninneko)
|
||||
- 6d495d2 #1014: Add server-side parameter handling for embeds (@whummer)
|
||||
- 5255804 #1091: Add caching for queries used in embeds (@whummer)
|
||||
|
||||
### Changed
|
||||
0314313 #1149: Presto QueryRunner supports tinyint and smallint (@toru-takahashi)
|
||||
8fa6fdb #1030: Make sure data sources list ordered by id (@arikfr)
|
||||
8df822e #1141: Make create data source button more prominent (@arikfr)
|
||||
96dd811 #1127: Mark basic_auth_password as secret (@adamlwgriffiths)
|
||||
ad65391 #1130: Improve Slack notification style (@AntoineAugusti)
|
||||
df637e3 #1116: Return meaningful error when there is no cached result. (@arikfr)
|
||||
65635ec #1102: Switch to HipChat V2 API (@arikfr)
|
||||
14fcf01 #1072: Remove counter from the tasks Done tab (as it always shows 50). #1047 (@arikfr)
|
||||
1a1160e #1062: DynamoDB: Better exception handling (@arikfr)
|
||||
ed45dcb #1044: Improve vagrant flow (@staritza)
|
||||
8b5dc8e #1036: Add optional block for more scripts in template (@arikfr)
|
||||
|
||||
- 0314313 #1149: Presto QueryRunner supports tinyint and smallint (@toru-takahashi)
|
||||
- 8fa6fdb #1030: Make sure data sources list ordered by id (@arikfr)
|
||||
- 8df822e #1141: Make create data source button more prominent (@arikfr)
|
||||
- 96dd811 #1127: Mark basic_auth_password as secret (@adamlwgriffiths)
|
||||
- ad65391 #1130: Improve Slack notification style (@AntoineAugusti)
|
||||
- df637e3 #1116: Return meaningful error when there is no cached result. (@arikfr)
|
||||
- 65635ec #1102: Switch to HipChat V2 API (@arikfr)
|
||||
- 14fcf01 #1072: Remove counter from the tasks Done tab (as it always shows 50). #1047 (@arikfr)
|
||||
- 1a1160e #1062: DynamoDB: Better exception handling (@arikfr)
|
||||
- ed45dcb #1044: Improve vagrant flow (@staritza)
|
||||
- 8b5dc8e #1036: Add optional block for more scripts in template (@arikfr)
|
||||
|
||||
### Fixed
|
||||
dbd48e1 #1143: Fix: use the email input type where needed (@ariarijp)
|
||||
7445972 #1142: Fix: dates in filters might be duplicated (@arikfr)
|
||||
5d0ed02 #1140: Fix: Hive should use the enabled variable (@arikfr)
|
||||
392627d #1139: Fix: Impala data source referencing wrong variable (@arikfr)
|
||||
c5bfbba #1133: Fix: query scrolling issues (@vishesh92)
|
||||
c01d266 #1128: Fix: visualization options not updating after changing type (@arikfr)
|
||||
6bc0e7a #1126: Fix #669: save fails when doing partial save of new query (@arikfr)
|
||||
3ce27b9 #1118: Fix: remove alerts for archived queries (@arikfr)
|
||||
4fabaae #1117: Fix #1052: filter not working for date/time values (@arikfr)
|
||||
c107c94 #1077: Fix: install needed dependencies to use Hive in Docker image (@nabilblk)
|
||||
abc790c #1115: Fix: allow non integers in alert reference value (@arikfr)
|
||||
4ec473c #1110: Fix #1109: mixed group permissions resulting in wrong permission (@arikfr)
|
||||
1ca5262 #1099: Fix RST syntax for links (@adamlwgriffiths)
|
||||
daa6c1c #1096: Fix typo in env variable VERSION_CHECK (@AntoineAugusti)
|
||||
cd06d27 #1095: Fix: use create_query permission for new query button. (@ordd)
|
||||
2bc0b27 #1061: Fix: area chart stacking doesn't work (@machira)
|
||||
8c21e91 #1108: Remove potnetially concurrency not safe code form enqueue_query (@arikfr)
|
||||
e831218 #1084: Fix #1049: duplicate alerts when data source belongs to multiple groups (@arikfr)
|
||||
6edb0ca #1080: Fix typo (@jeffwidman)
|
||||
64d7538 #1074: Fix: ElasticSearch wasn't using correct type names (@toyama0919)
|
||||
3f90dd9 #1064: Fix: old task trackers were not really removed (@arikfr)
|
||||
e10ecd2 #1058: Bring back filters if dashboard filters are enabled (@AntoineAugusti)
|
||||
701035f #1059: Fix: DynamoDB having issues when setting host (@arikfr)
|
||||
2924d4f #1040: Small fixes to visualizations view (@arikfr)
|
||||
fec0d5f #1037: Fix: multi filter wasn't working with __ syntax (@dheerajrav)
|
||||
b066ce4 #1033: Fix: only ask for notification permissions if wasn't denied (@arikfr)
|
||||
960c416 #1032: Fix: make sure we return dashboards only for current org only (@arikfr)
|
||||
b3844d3 #1029: Hive: close connection only if it exists (@arikfr)
|
||||
|
||||
- dbd48e1 #1143: Fix: use the email input type where needed (@ariarijp)
|
||||
- 7445972 #1142: Fix: dates in filters might be duplicated (@arikfr)
|
||||
- 5d0ed02 #1140: Fix: Hive should use the enabled variable (@arikfr)
|
||||
- 392627d #1139: Fix: Impala data source referencing wrong variable (@arikfr)
|
||||
- c5bfbba #1133: Fix: query scrolling issues (@vishesh92)
|
||||
- c01d266 #1128: Fix: visualization options not updating after changing type (@arikfr)
|
||||
- 6bc0e7a #1126: Fix #669: save fails when doing partial save of new query (@arikfr)
|
||||
- 3ce27b9 #1118: Fix: remove alerts for archived queries (@arikfr)
|
||||
- 4fabaae #1117: Fix #1052: filter not working for date/time values (@arikfr)
|
||||
- c107c94 #1077: Fix: install needed dependencies to use Hive in Docker image (@nabilblk)
|
||||
- abc790c #1115: Fix: allow non integers in alert reference value (@arikfr)
|
||||
- 4ec473c #1110: Fix #1109: mixed group permissions resulting in wrong permission (@arikfr)
|
||||
- 1ca5262 #1099: Fix RST syntax for links (@adamlwgriffiths)
|
||||
- daa6c1c #1096: Fix typo in env variable VERSION_CHECK (@AntoineAugusti)
|
||||
- cd06d27 #1095: Fix: use create_query permission for new query button. (@ordd)
|
||||
- 2bc0b27 #1061: Fix: area chart stacking doesn't work (@machira)
|
||||
- 8c21e91 #1108: Remove potnetially concurrency not safe code form enqueue_query (@arikfr)
|
||||
- e831218 #1084: Fix #1049: duplicate alerts when data source belongs to multiple groups (@arikfr)
|
||||
- 6edb0ca #1080: Fix typo (@jeffwidman)
|
||||
- 64d7538 #1074: Fix: ElasticSearch wasn't using correct type names (@toyama0919)
|
||||
- 3f90dd9 #1064: Fix: old task trackers were not really removed (@arikfr)
|
||||
- e10ecd2 #1058: Bring back filters if dashboard filters are enabled (@AntoineAugusti)
|
||||
- 701035f #1059: Fix: DynamoDB having issues when setting host (@arikfr)
|
||||
- 2924d4f #1040: Small fixes to visualizations view (@arikfr)
|
||||
- fec0d5f #1037: Fix: multi filter wasn't working with __ syntax (@dheerajrav)
|
||||
- b066ce4 #1033: Fix: only ask for notification permissions if wasn't denied (@arikfr)
|
||||
- 960c416 #1032: Fix: make sure we return dashboards only for current org only (@arikfr)
|
||||
- b3844d3 #1029: Hive: close connection only if it exists (@arikfr)
|
||||
|
||||
### Docs
|
||||
6bb09d8 #1146: Docs: add a link to settings documentation. (@adamlwgriffiths)
|
||||
095e759 #1103: Docs: add section about monitoring (@AntoineAugusti)
|
||||
e942486 #1090: Contributing Guide (@arikfr)
|
||||
3037c4f #1066: Docs: command type-o fix. (@edwardsharp)
|
||||
2ee0065 #1038: Add an ISSUE_TEMPLATE.md to direct people at the forum (@arikfr)
|
||||
f7322a4 #1021: Vagrant docs: add purging the cache step (@ariarijp)
|
||||
|
||||
- 6bb09d8 #1146: Docs: add a link to settings documentation. (@adamlwgriffiths)
|
||||
- 095e759 #1103: Docs: add section about monitoring (@AntoineAugusti)
|
||||
- e942486 #1090: Contributing Guide (@arikfr)
|
||||
- 3037c4f #1066: Docs: command type-o fix. (@edwardsharp)
|
||||
- 2ee0065 #1038: Add an ISSUE_TEMPLATE.md to direct people at the forum (@arikfr)
|
||||
- f7322a4 #1021: Vagrant docs: add purging the cache step (@ariarijp)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ The following is a set of guidelines for contributing to Redash. These are guide
|
||||
|
||||
## Quick Links:
|
||||
|
||||
- [Feature Roadmap](https://trello.com/b/b2LUHU7A/re-dash-roadmap)
|
||||
- [Feature Roadmap](https://trello.com/b/b2LUHU7A/redash-roadmap)
|
||||
- [Feature Requests](https://discuss.redash.io/c/feature-requests)
|
||||
- [Gitter Chat](https://gitter.im/getredash/redash) or [Slack](https://slack.redash.io)
|
||||
- [Documentation](https://redash.io/help/)
|
||||
@@ -29,7 +29,7 @@ The following is a set of guidelines for contributing to Redash. These are guide
|
||||
- [Documentation](#documentation)
|
||||
- Design?
|
||||
|
||||
[Addtional Notes](#additional-notes)
|
||||
[Additional Notes](#additional-notes)
|
||||
|
||||
- [Release Method](#release-method)
|
||||
- [Code of Conduct](#code-of-conduct)
|
||||
@@ -46,9 +46,9 @@ When creating a new bug report, please make sure to:
|
||||
|
||||
### Suggesting Enhancements / Feature Requests
|
||||
|
||||
If you would like to suggest an enchancement or ask for a new feature:
|
||||
If you would like to suggest an enhancement or ask for a new feature:
|
||||
|
||||
- Please check [the roadmap](https://trello.com/b/b2LUHU7A/re-dash-roadmap) for existing Trello card for what you want to suggest/ask. If there is, feel free to upvote it to signal interest or add your comments.
|
||||
- Please check [the roadmap](https://trello.com/b/b2LUHU7A/redash-roadmap) for existing Trello card for what you want to suggest/ask. If there is, feel free to upvote it to signal interest or add your comments.
|
||||
- If there is no existing card, open a thread in [the forum](https://discuss.redash.io/c/feature-requests) to start a discussion about what you want to suggest. Try to provide as much details and context as possible and include information about *the problem you want to solve* rather only *your proposed solution*.
|
||||
|
||||
### Pull Requests
|
||||
@@ -56,7 +56,7 @@ If you would like to suggest an enchancement or ask for a new feature:
|
||||
- **Code contributions are welcomed**. For big changes or significant features, it's usually better to reach out first and discuss what you want to implement and how (we recommend reading: [Pull Request First](https://medium.com/practical-blend/pull-request-first-f6bb667a9b6#.ozlqxvj36)). This to make sure that what you want to implement is aligned with our goals for the project and that no one else is already working on it.
|
||||
- Include screenshots and animated GIFs in your pull request whenever possible.
|
||||
- Please add [documentation](#documentation) for new features or changes in functionality along with the code.
|
||||
- Please follow existing code style. We use PEP8 for Python and sensible style for Javascript.
|
||||
- Please follow existing code style. We use PEP8 for Python and sensible style for JavaScript.
|
||||
|
||||
### Documentation
|
||||
|
||||
|
||||
3
Makefile
3
Makefile
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ scheduler() {
|
||||
}
|
||||
|
||||
server() {
|
||||
exec /usr/local/bin/gunicorn -b 0.0.0.0:5000 --name redash -w4 redash.wsgi:app
|
||||
exec /usr/local/bin/gunicorn -b 0.0.0.0:5000 --name redash -w${REDASH_WEB_WORKERS:-4} redash.wsgi:app
|
||||
}
|
||||
|
||||
help() {
|
||||
@@ -72,7 +72,10 @@ case "$1" in
|
||||
tests)
|
||||
tests
|
||||
;;
|
||||
*)
|
||||
help)
|
||||
help
|
||||
;;
|
||||
*)
|
||||
exec "$@"
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import namedtuple
|
||||
@@ -111,7 +111,10 @@ def restart_services():
|
||||
# otherwise it won't notice that /opt/redash/current pointing at a different
|
||||
# directory.
|
||||
green("Restarting...")
|
||||
run('sudo /etc/init.d/redash_supervisord restart')
|
||||
try:
|
||||
run('sudo /etc/init.d/redash_supervisord restart')
|
||||
except subprocess.CalledProcessError as e:
|
||||
run('sudo service supervisor restart')
|
||||
|
||||
|
||||
def update_requirements(version_name):
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"presets": ["es2015", "stage-2"],
|
||||
"plugins": ["transform-object-assign"]
|
||||
"plugins": ["angularjs-annotate", "transform-object-assign"]
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
build/*.js
|
||||
config/*.js
|
||||
node_modules
|
||||
|
||||
@@ -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,
|
||||
}]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -437,6 +437,10 @@ counter-renderer counter-name {
|
||||
border: 1px solid rgba(0,0,0,.15);
|
||||
}
|
||||
|
||||
.parameter-label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
div.table-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
113
client/app/components/alerts/alert-subscriptions/index.js
Normal file
113
client/app/components/alerts/alert-subscriptions/index.js
Normal 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> ${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,
|
||||
}));
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="/"><img ng-src="{{$ctrl.logoUrl}}"/></a>
|
||||
<a class="navbar-brand" ng-href="{{$ctrl.basePath}}"><img ng-src="{{$ctrl.logoUrl}}"/></a>
|
||||
</div>
|
||||
<div class="collapse navbar-collapse" uib-collapse="!isNavOpen">
|
||||
<ul class="nav navbar-nav">
|
||||
@@ -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>
|
||||
@@ -1,16 +1,17 @@
|
||||
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');
|
||||
|
||||
function controller($rootScope, $location, $uibModal, Auth, currentUser, Dashboard) {
|
||||
// TODO: logoUrl should come from clientconfig
|
||||
function controller($rootScope, $location, $uibModal, Auth, currentUser, clientConfig, Dashboard) {
|
||||
this.logoUrl = logoUrl;
|
||||
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, Dashboa
|
||||
};
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('appHeader', {
|
||||
template,
|
||||
controller,
|
||||
|
||||
@@ -27,6 +27,6 @@ function cancelQueryButton() {
|
||||
};
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.directive('cancelQueryButton', cancelQueryButton);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -4,7 +4,14 @@
|
||||
</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">
|
||||
<label>
|
||||
<input name="input" type="checkbox" ng-model="$ctrl.dashboard.dashboard_filters_enabled">
|
||||
Use Dashboard Level Filters
|
||||
</label>
|
||||
</p>
|
||||
|
||||
<div gridster="$ctrl.gridsterOptions" ng-if="$ctrl.items | notEmpty">
|
||||
@@ -17,5 +24,5 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" ng-disabled="$ctrl.saveInProgress" ng-click="$ctrl.dismiss()">Close</button>
|
||||
<button type="button" class="btn btn-primary" ng-disabled="$ctrl.saveInProgress" ng-click="$ctrl.saveDashboard()">Save</button>
|
||||
<button type="button" class="btn btn-primary" ng-disabled="$ctrl.saveInProgress || !$ctrl.isFormValid()" ng-click="$ctrl.saveDashboard()">Save</button>
|
||||
</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
import { sortBy } from 'underscore';
|
||||
import { isEmpty, sortBy } from 'underscore';
|
||||
import template from './edit-dashboard-dialog.html';
|
||||
|
||||
const EditDashboardDialog = {
|
||||
@@ -45,6 +45,8 @@ const EditDashboardDialog = {
|
||||
});
|
||||
}
|
||||
|
||||
this.isFormValid = () => !isEmpty(this.dashboard.name);
|
||||
|
||||
this.saveDashboard = () => {
|
||||
this.saveInProgress = true;
|
||||
|
||||
@@ -65,6 +67,7 @@ const EditDashboardDialog = {
|
||||
slug: this.dashboard.id,
|
||||
name: this.dashboard.name,
|
||||
version: this.dashboard.version,
|
||||
dashboard_filters_enabled: this.dashboard.dashboard_filters_enabled,
|
||||
layout: JSON.stringify(layout),
|
||||
};
|
||||
|
||||
@@ -96,6 +99,6 @@ const EditDashboardDialog = {
|
||||
},
|
||||
};
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('editDashboardDialog', EditDashboardDialog);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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: {
|
||||
@@ -78,8 +85,8 @@ function DashboardWidgetCtrl($location, $uibModal, $window, Events, currentUser)
|
||||
};
|
||||
|
||||
if (this.widget.visualization) {
|
||||
Events.record('view', 'query', this.widget.visualization.query.id);
|
||||
Events.record('view', 'visualization', this.widget.visualization.id);
|
||||
Events.record('view', 'query', this.widget.visualization.query.id, { dashboard: true });
|
||||
Events.record('view', 'visualization', this.widget.visualization.id, { dashboard: true });
|
||||
|
||||
this.query = this.widget.getQuery();
|
||||
this.reload(false);
|
||||
@@ -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,
|
||||
@@ -1,27 +1,28 @@
|
||||
<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" autofocus></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="dataSourceName">Name</label>
|
||||
<input type="string" class="form-control" name="dataSourceName" ng-model="target.name" required>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<div class="form-group" ng-class='{"has-error": !inner.input.$valid}' ng-form="inner" ng-repeat="(name, input) in type.configuration_schema.properties">
|
||||
<label ng-if="input.type !== 'checkbox'">{{input.title || name | capitalize}}</label>
|
||||
<input name="input" type="{{input.type}}" class="form-control" ng-model="target.options[name]" ng-required="input.required"
|
||||
ng-if="input.type !== 'file' && input.type !== 'checkbox'" accesskey="tab" placeholder="{{input.default}}">
|
||||
<div class="form-group" ng-class='{"has-error": !inner.input.$valid}' ng-form="inner" ng-repeat="field in fields">
|
||||
<label ng-if="field.property.type !== 'checkbox'">{{field.property.title || field.name | capitalize}}</label>
|
||||
<input name="input" type="{{field.property.type}}" class="form-control" ng-model="target.options[field.name]" ng-required="field.property.required"
|
||||
ng-if="field.property.type !== 'file' && field.property.type !== 'checkbox'" accesskey="tab" placeholder="{{field.property.default}}">
|
||||
|
||||
<label ng-if="input.type=='checkbox'">
|
||||
<input name="input" type="{{input.type}}" ng-model="target.options[name]" ng-required="input.required"
|
||||
ng-if="input.type !== 'file'" accesskey="tab" placeholder="{{input.default}}">
|
||||
{{input.title || name | capitalize}}
|
||||
<label ng-if="field.property.type=='checkbox'">
|
||||
<input name="input" type="{{field.property.type}}" ng-model="target.options[field.name]" ng-required="field.property.required"
|
||||
ng-if="field.property.type !== 'file'" accesskey="tab" placeholder="{{field.property.default}}">
|
||||
{{field.property.title || field.name | capitalize}}
|
||||
</label>
|
||||
|
||||
<input name="input" type="file" class="form-control" ng-model="files[name]" ng-required="input.required && !target.options[name]"
|
||||
<input name="input" type="file" class="form-control" ng-model="files[field.name]" ng-required="field.property.required && !target.options[field.name]"
|
||||
base-sixty-four-input
|
||||
ng-if="input.type === 'file'">
|
||||
ng-if="field.property.type === 'file'">
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" ng-disabled="!dataSourceForm.$valid" ng-click="saveChanges()">Save</button>
|
||||
<span ng-repeat="action in actions">
|
||||
<button class="btn"
|
||||
|
||||
@@ -1,8 +1,22 @@
|
||||
import { each, contains, find } from 'underscore';
|
||||
import { isUndefined, each, contains, find } from 'underscore';
|
||||
import endsWith from 'underscore.string/endsWith';
|
||||
import template from './dynamic-form.html';
|
||||
|
||||
function DynamicForm($http, toastr, $q) {
|
||||
function orderedInputs(properties, order) {
|
||||
const inputs = new Array(order.length);
|
||||
Object.keys(properties).forEach((key) => {
|
||||
const position = order.indexOf(key);
|
||||
const input = { name: key, property: properties[key] };
|
||||
if (position > -1) {
|
||||
inputs[position] = input;
|
||||
} else {
|
||||
inputs.push(input);
|
||||
}
|
||||
});
|
||||
return inputs;
|
||||
}
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: 'true',
|
||||
@@ -19,7 +33,15 @@ function DynamicForm($http, toastr, $q) {
|
||||
$scope.target.type = types[0].type;
|
||||
}
|
||||
|
||||
$scope.type = find(types, t => t.type === $scope.target.type);
|
||||
const type = find(types, t => t.type === $scope.target.type);
|
||||
const configurationSchema = type.configuration_schema;
|
||||
|
||||
$scope.fields = orderedInputs(
|
||||
configurationSchema.properties,
|
||||
configurationSchema.order || [],
|
||||
);
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
$scope.inProgressActions = {};
|
||||
@@ -35,6 +57,7 @@ function DynamicForm($http, toastr, $q) {
|
||||
$scope.inProgressActions[action.name] = false;
|
||||
action.name = name;
|
||||
}
|
||||
|
||||
originalCallback(release);
|
||||
};
|
||||
});
|
||||
@@ -80,29 +103,47 @@ function DynamicForm($http, toastr, $q) {
|
||||
prop.required = contains(type.configuration_schema.required, name);
|
||||
});
|
||||
});
|
||||
|
||||
$scope.$watch('target.type', (current, prev) => {
|
||||
if (prev !== current) {
|
||||
if (prev !== undefined) {
|
||||
$scope.target.options = {};
|
||||
}
|
||||
|
||||
const type = setType($scope.types);
|
||||
|
||||
if (Object.keys($scope.target.options).length === 0) {
|
||||
const properties = type.configuration_schema.properties;
|
||||
Object.keys(properties).forEach((property) => {
|
||||
if (!isUndefined(properties[property].default)) {
|
||||
$scope.target.options[property] = properties[property].default;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$scope.$watch('target.type', (current, prev) => {
|
||||
if (prev !== current) {
|
||||
if (prev !== undefined) {
|
||||
$scope.target.options = {};
|
||||
}
|
||||
setType($scope.types);
|
||||
}
|
||||
});
|
||||
|
||||
$scope.saveChanges = () => {
|
||||
$scope.target.$save(() => {
|
||||
toastr.success('Saved.');
|
||||
$scope.dataSourceForm.$setPristine();
|
||||
}, () => {
|
||||
toastr.error('Failed saving.');
|
||||
});
|
||||
$scope.target.$save(
|
||||
() => {
|
||||
toastr.success('Saved.');
|
||||
$scope.dataSourceForm.$setPristine();
|
||||
},
|
||||
(error) => {
|
||||
if (error.status === 400 && 'message' in error.data) {
|
||||
toastr.error(error.data.message);
|
||||
} else {
|
||||
toastr.error('Failed saving.');
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.directive('dynamicForm', DynamicForm);
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ function DynamicTable($sanitize) {
|
||||
};
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('dynamicTable', {
|
||||
template,
|
||||
controller: DynamicTable,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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: '<',
|
||||
|
||||
@@ -15,6 +15,6 @@ const ErrorMessagesComponent = {
|
||||
},
|
||||
};
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('errorMessages', ErrorMessagesComponent);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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">
|
||||
@@ -34,6 +34,6 @@ const EditGroupDialogComponent = {
|
||||
},
|
||||
};
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('editGroupDialog', EditGroupDialogComponent);
|
||||
}
|
||||
@@ -15,7 +15,7 @@ function controller($window, $location, toastr, currentUser) {
|
||||
};
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('groupName', {
|
||||
bindings: {
|
||||
group: '<',
|
||||
@@ -1,20 +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';
|
||||
@@ -11,6 +11,6 @@ const Overlay = {
|
||||
transclude: true,
|
||||
};
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('overlay', Overlay);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ function controller() {
|
||||
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('pageHeader', {
|
||||
template,
|
||||
controller,
|
||||
|
||||
@@ -7,7 +7,7 @@ class PaginatorCtrl {
|
||||
}
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('paginator', {
|
||||
template: `
|
||||
<div class="text-center">
|
||||
|
||||
@@ -13,14 +13,33 @@
|
||||
<select ng-model="$ctrl.parameter.type" class="form-control">
|
||||
<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>
|
||||
|
||||
@@ -1,11 +1,27 @@
|
||||
<div class="form-inline bg-white p-5" ng-if="parameters | notEmpty" ui-sortable="{ 'ui-floating': true, 'disabled': !editable }" ng-model="parameters">
|
||||
<div class="form-group" ng-repeat="param in parameters">
|
||||
<label>{{param.title}}</label>
|
||||
<button class="btn btn-default btn-xs" ng-click="showParameterSettings(param)" ng-if="editable"><i class="zmdi zmdi-settings"></i></button>
|
||||
<div class="form-inline bg-white p-5"
|
||||
ng-if="parameters | notEmpty"
|
||||
ui-sortable="{ 'ui-floating': true, 'disabled': !editable }"
|
||||
ng-model="parameters">
|
||||
<div class="form-group m-l-10 m-r-10"
|
||||
ng-repeat="param in parameters">
|
||||
<label class="parameter-label">{{param.title}}</label>
|
||||
<button class="btn btn-default btn-xs"
|
||||
ng-click="showParameterSettings(param)"
|
||||
ng-if="editable">
|
||||
<i class="zmdi zmdi-settings"></i>
|
||||
</button>
|
||||
<span ng-switch="param.type">
|
||||
<input ng-switch-when="datetime-with-seconds" type="datetime-local" step="1" class="form-control" ng-model="param.ngModel">
|
||||
<input ng-switch-when="datetime-local" type="datetime-local" class="form-control" ng-model="param.ngModel">
|
||||
<input ng-switch-when="date" type="date" class="form-control" ng-model="param.ngModel">
|
||||
<span ng-switch-when="enum">
|
||||
<select ng-model="param.value" class="form-control">
|
||||
<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>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -41,6 +129,14 @@ function ParametersDirective($location, $uibModal) {
|
||||
}, true);
|
||||
}
|
||||
|
||||
// These are input as newline delimited values,
|
||||
// so we split them here.
|
||||
scope.extractEnumOptions = (enumOptions) => {
|
||||
if (enumOptions) {
|
||||
return enumOptions.split('\n');
|
||||
}
|
||||
return [];
|
||||
};
|
||||
scope.showParameterSettings = (param) => {
|
||||
$uibModal.open({
|
||||
component: 'parameterSettings',
|
||||
@@ -53,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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,6 @@ function alertUnsavedChanges($window) {
|
||||
};
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.directive('alertUnsavedChanges', alertUnsavedChanges);
|
||||
}
|
||||
37
client/app/components/queries/api-key-dialog.js
Normal file
37
client/app/components/queries/api-key-dialog.js
Normal file
@@ -0,0 +1,37 @@
|
||||
const ApiKeyDialog = {
|
||||
template: `<div class="modal-header">
|
||||
<button type="button" class="close" aria-label="Close" ng-click="$ctrl.close()"><span aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h5>API Key</h5>
|
||||
<pre>{{$ctrl.apiKey}}</pre>
|
||||
|
||||
<h5>Example API Calls:</h5>
|
||||
|
||||
<div>
|
||||
Results in CSV format:
|
||||
|
||||
<pre>{{$ctrl.csvUrl}}</pre>
|
||||
|
||||
Results in JSON format:
|
||||
|
||||
<pre>{{$ctrl.jsonUrl}}</pre>
|
||||
</div>
|
||||
</div>`,
|
||||
controller(clientConfig) {
|
||||
'ngInject';
|
||||
|
||||
this.apiKey = this.resolve.query.api_key;
|
||||
this.csvUrl = `${clientConfig.basePath}api/queries/${this.resolve.query.id}/results.csv?api_key=${this.apiKey}`;
|
||||
this.jsonUrl = `${clientConfig.basePath}api/queries/${this.resolve.query.id}/results.json?api_key=${this.apiKey}`;
|
||||
},
|
||||
bindings: {
|
||||
resolve: '<',
|
||||
close: '&',
|
||||
dismiss: '&',
|
||||
},
|
||||
};
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('apiKeyDialog', ApiKeyDialog);
|
||||
}
|
||||
17
client/app/components/queries/embed-code-dialog.html
Normal file
17
client/app/components/queries/embed-code-dialog.html
Normal 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">×</span></button>
|
||||
<h4 class="modal-title">Embed Code</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h5>IFrame Embed</h5>
|
||||
<div>
|
||||
<code><iframe src="{{ $ctrl.embedUrl }}" width="720" height="391"></iframe></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>
|
||||
@@ -20,6 +20,6 @@ const EmbedCodeDialog = {
|
||||
template,
|
||||
};
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('embedCodeDialog', EmbedCodeDialog);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -21,6 +21,6 @@ function queryResultLink() {
|
||||
};
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.directive('queryResultLink', queryResultLink);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -28,6 +28,6 @@ const SchemaBrowser = {
|
||||
template,
|
||||
};
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('schemaBrowser', SchemaBrowser);
|
||||
}
|
||||
@@ -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) {
|
||||
@@ -36,8 +36,8 @@ export default function (ngModule) {
|
||||
function loadData($http, $route, $q, Auth) {
|
||||
return session($http, $route, Auth).then(() => {
|
||||
const queryId = $route.current.params.queryId;
|
||||
const query = $http.get(`/api/queries/${queryId}`).then(response => response.data);
|
||||
const queryResult = $http.get(`/api/queries/${queryId}/results.json`).then(response => response.data);
|
||||
const query = $http.get(`api/queries/${queryId}`).then(response => response.data);
|
||||
const queryResult = $http.get(`api/queries/${queryId}/results.json${location.search}`).then(response => response.data);
|
||||
return $q.all([query, queryResult]);
|
||||
});
|
||||
}
|
||||
2
client/app/components/query-based-parameter.html
Normal file
2
client/app/components/query-based-parameter.html
Normal 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>
|
||||
@@ -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: '<',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,6 @@ const RdTimeAgo = {
|
||||
'</span>',
|
||||
};
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('rdTimeAgo', RdTimeAgo);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,6 @@ function rdTimer() {
|
||||
};
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.directive('rdTimer', rdTimer);
|
||||
}
|
||||
|
||||
@@ -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.',
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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');
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
26
client/app/components/sort-icon.js
Normal file
26
client/app/components/sort-icon.js
Normal file
@@ -0,0 +1,26 @@
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('sortIcon', {
|
||||
template: '<span ng-if="$ctrl.showIcon"><i class="fa fa-sort-{{$ctrl.icon}}"></i></span>',
|
||||
bindings: {
|
||||
column: '<',
|
||||
sortColumn: '<',
|
||||
reverse: '<',
|
||||
},
|
||||
controller() {
|
||||
this.$onChanges = (changes) => {
|
||||
['column', 'sortColumn', 'reverse'].forEach((v) => {
|
||||
if (v in changes) {
|
||||
this[v] = changes[v].currentValue;
|
||||
}
|
||||
});
|
||||
|
||||
this.showIcon = false;
|
||||
|
||||
if (this.column === this.sortColumn) {
|
||||
this.showIcon = true;
|
||||
this.icon = this.reverse ? 'desc' : 'asc';
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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>' +
|
||||
|
||||
@@ -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
111
client/app/config/index.js
Normal 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;
|
||||
11
client/app/config/styles.js
Normal file
11
client/app/config/styles.js
Normal 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';
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html ng-app="app">
|
||||
<html ng-app="app" ng-strict-di>
|
||||
<head lang="en">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta charset="UTF-8">
|
||||
|
||||
@@ -1,105 +1,8 @@
|
||||
// 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);
|
||||
uiSelectConfig.theme = 'bootstrap';
|
||||
@@ -111,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');
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { sortBy } from 'underscore';
|
||||
|
||||
export default class Paginator {
|
||||
constructor(rows, { page = 1, itemsPerPage = 20, totalCount = undefined } = {}) {
|
||||
this.page = page;
|
||||
this.itemsPerPage = itemsPerPage;
|
||||
this.updateRows(rows, totalCount);
|
||||
this.orderByField = undefined;
|
||||
this.orderByReverse = false;
|
||||
}
|
||||
|
||||
setPage(page) {
|
||||
@@ -24,4 +28,20 @@ export default class Paginator {
|
||||
this.totalCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
orderBy(column) {
|
||||
if (column === this.orderByField) {
|
||||
this.orderByReverse = !this.orderByReverse;
|
||||
} else {
|
||||
this.orderByField = column;
|
||||
this.orderByReverse = false;
|
||||
}
|
||||
|
||||
if (this.orderByField) {
|
||||
this.rows = sortBy(this.rows, this.orderByField);
|
||||
if (this.orderByReverse) {
|
||||
this.rows = this.rows.reverse();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
328
client/app/lib/visualizations/d3box.js
vendored
Normal file
328
client/app/lib/visualizations/d3box.js
vendored
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
19
client/app/multi_org.html
Normal file
19
client/app/multi_org.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html ng-app="app" ng-strict-di>
|
||||
<head lang="en">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta charset="UTF-8">
|
||||
<base href="{{base_href}}">
|
||||
<title>Redash</title>
|
||||
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/images/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16.png">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<section>
|
||||
<div ng-view></div>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>',
|
||||
|
||||
@@ -35,14 +35,14 @@
|
||||
<td>{{row.data_source_id}}</td>
|
||||
<td>{{row.username}}</td>
|
||||
<td>{{row.state}} <span ng-if="row.state === 'failed'" uib-popover="{{row.error}}" popover-trigger="mouseenter" class="zmdi zmdi-help"></span></td>
|
||||
<td>{{row.query_id}}</td>
|
||||
<td><a href="queries/{{row.query_id}}">{{row.query_id}}</a></td>
|
||||
<td>{{row.query_hash}}</td>
|
||||
<td>{{row.run_time | durationHumanize}}</td>
|
||||
<td>{{row.created_at | toMilliseconds | dateTime }}</td>
|
||||
<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>
|
||||
|
||||
@@ -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> ${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,
|
||||
});
|
||||
@@ -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>',
|
||||
|
||||
@@ -4,20 +4,20 @@
|
||||
</page-header>
|
||||
|
||||
<div class="container">
|
||||
<div class="container bg-white">
|
||||
<div class="bg-white">
|
||||
<table class="table table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Created By</th>
|
||||
<th>State</th>
|
||||
<th>Created At</th>
|
||||
<th class="sortable-column" ng-click="$ctrl.alerts.orderBy('name')">Name <sort-icon column="'name'" sort-column="$ctrl.alerts.orderByField" reverse="$ctrl.alerts.orderByReverse"></sort-icon></th>
|
||||
<th class="sortable-column" ng-click="$ctrl.alerts.orderBy('created_by')">Created By <sort-icon column="'created_by'" sort-column="$ctrl.alerts.orderByField" reverse="$ctrl.alerts.orderByReverse"></sort-icon></th>
|
||||
<th class="sortable-column" ng-click="$ctrl.alerts.orderBy('state')">State <sort-icon column="'state'" sort-column="$ctrl.alerts.orderByField" reverse="$ctrl.alerts.orderByReverse"></sort-icon></th>
|
||||
<th class="sortable-column" ng-click="$ctrl.alerts.orderBy('created_at')">Created By <sort-icon column="'created_at'" sort-column="$ctrl.alerts.orderByField" reverse="$ctrl.alerts.orderByReverse"></sort-icon></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="row in $ctrl.alerts.getPageRows()">
|
||||
<td><a href="alerts/{{row.id}}">{{row.name}}</a></td>
|
||||
<td>{{row.user.name}}</td>
|
||||
<td>{{row.created_by}}</td>
|
||||
<td><span ng-class="row.class">{{row.state | uppercase}}</span> since <span am-time-ago="row.updated_at"></span></td>
|
||||
<td><span am-time-ago="row.created_at"></span></td>
|
||||
</tr>
|
||||
|
||||
@@ -1,29 +1,32 @@
|
||||
import { Paginator } from '../../utils';
|
||||
import { Paginator } from '@/lib/pagination';
|
||||
import template from './alerts-list.html';
|
||||
|
||||
const stateClass = {
|
||||
ok: 'label label-success',
|
||||
triggered: 'label label-danger',
|
||||
unknown: 'label label-warning',
|
||||
};
|
||||
|
||||
class AlertsListCtrl {
|
||||
constructor(Events, Alert) {
|
||||
Events.record('view', 'page', 'alerts');
|
||||
|
||||
this.alerts = new Paginator([], { itemsPerPage: 20 });
|
||||
|
||||
Alert.query((alerts) => {
|
||||
const stateClass = {
|
||||
ok: 'label label-success',
|
||||
triggered: 'label label-danger',
|
||||
unknown: 'label label-warning',
|
||||
};
|
||||
|
||||
alerts.forEach((alert) => {
|
||||
alert.class = stateClass[alert.state];
|
||||
});
|
||||
|
||||
this.alerts.updateRows(alerts);
|
||||
this.alerts.updateRows(alerts.map(alert => ({
|
||||
id: alert.id,
|
||||
name: alert.name,
|
||||
state: alert.state,
|
||||
class: stateClass[alert.state],
|
||||
created_by: alert.user.name,
|
||||
created_at: alert.created_at,
|
||||
updated_at: alert.updated_at,
|
||||
})));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('alertsListPage', {
|
||||
template,
|
||||
controller: AlertsListCtrl,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -46,6 +46,7 @@ function DashboardListCtrl(Dashboard, $location, clientConfig) {
|
||||
|
||||
this.update = () => {
|
||||
this.dashboards.$promise.then((data) => {
|
||||
data = _.sortBy(data, 'name');
|
||||
const filteredDashboards = data.map((dashboard) => {
|
||||
dashboard.tags = (dashboard.name.match(TAGS_REGEX) || []).map(tag => tag.replace(/:$/, ''));
|
||||
dashboard.untagged_name = dashboard.name.replace(TAGS_REGEX, '').trim();
|
||||
@@ -74,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,
|
||||
|
||||
@@ -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;
|
||||
@@ -17,6 +19,8 @@ function DashboardCtrl($rootScope, $routeParams, $location, $timeout, $q, $uibMo
|
||||
{ name: '10 minutes', rate: 60 * 10 },
|
||||
{ name: '30 minutes', rate: 60 * 30 },
|
||||
{ name: '1 hour', rate: 60 * 60 },
|
||||
{ name: '12 hour', rate: 12 * 60 * 60 },
|
||||
{ name: '24 hour', rate: 24 * 60 * 60 },
|
||||
];
|
||||
|
||||
this.setRefreshRate = (rate) => {
|
||||
@@ -40,8 +44,7 @@ function DashboardCtrl($rootScope, $routeParams, $location, $timeout, $q, $uibMo
|
||||
globalParams[param.name].locals.push(param);
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
}));
|
||||
this.globalParameters = _.values(globalParams);
|
||||
};
|
||||
|
||||
@@ -58,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();
|
||||
|
||||
@@ -113,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);
|
||||
@@ -126,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 = () => {
|
||||
@@ -155,12 +156,20 @@ function DashboardCtrl($rootScope, $routeParams, $location, $timeout, $q, $uibMo
|
||||
};
|
||||
|
||||
this.editDashboard = () => {
|
||||
const previousFiltersState = this.dashboard.dashboard_filters_enabled;
|
||||
$uibModal.open({
|
||||
component: 'editDashboardDialog',
|
||||
resolve: {
|
||||
dashboard: () => this.dashboard,
|
||||
},
|
||||
}).result.then((dashboard) => { this.dashboard = dashboard; });
|
||||
}).result.then((dashboard) => {
|
||||
const shouldRenderDashboard = !previousFiltersState && dashboard.dashboard_filters_enabled;
|
||||
this.dashboard = dashboard;
|
||||
|
||||
if (shouldRenderDashboard) {
|
||||
renderDashboard(this.dashboard);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.addWidget = () => {
|
||||
@@ -250,7 +259,7 @@ const ShareDashboardComponent = {
|
||||
},
|
||||
};
|
||||
|
||||
export default function (ngModule) {
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('shareDashboard', ShareDashboardComponent);
|
||||
ngModule.component('dashboardPage', {
|
||||
template,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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 [];
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import registerList from './list';
|
||||
import registerShow from './show';
|
||||
|
||||
export default function (ngModule) {
|
||||
return Object.assign({}, registerList(ngModule), registerShow(ngModule));
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user