Compare commits

...

314 Commits

Author SHA1 Message Date
Arik Fraimovich
a16f551e22 Pin Cypress version (#4284) 2019-10-27 15:02:03 +02:00
Arik Fraimovich
e94515d340 Updated package-lock.json file 2019-10-27 14:23:12 +02:00
Arik Fraimovich
8de1fa3318 Make the build-docker-image step take approval 2019-10-27 13:44:18 +02:00
Arik Fraimovich
6227a1d071 Remove beta tag 2019-10-27 13:43:10 +02:00
Arik Fraimovich
13b6bfc55f CHANGELOG for v8.0.0-beta.2 (#4145)
* Stop building tarballs.

* Update version reference.

* CHANGELOG for 8.0.0-beta.2
2019-10-27 13:42:37 +02:00
Levko Kravets
f5802d2dec Widget filters overlapped by visualization (#4137)
* Fix: widget filters overlapped by visualization

* Fix tests

* Fix tests
2019-10-27 13:42:37 +02:00
Levko Kravets
ba0ccebe58 Color picker component (#4136) 2019-10-27 13:42:37 +02:00
Gabriel Dutra
c5a65b3321 Query Snippets: Use onClick instead of link for 'Click here' option (#4144)
* Snippets: Don't change url when not needed

* Revert "Snippets: Don't change url when not needed"

This reverts commit 2f346f3bb4.

* Query Snippets: use onClick instead of link
2019-10-27 13:42:37 +02:00
Ran Byron
c622a76f3a Bug fix: Query view doesn't sync parameters when selecting and deleting (#4146) 2019-10-27 13:42:37 +02:00
Arik Fraimovich
76e0fa6e9c CHANGELOG for V8-beta. (#4057)
* CHANGELOG for V8-beta.

* Update CHANGELOG.md

* Update CHANGELOG.md

* Update CHANGELOG.md
2019-10-27 13:42:37 +02:00
Arik Fraimovich
f0ba045913 Allow users to share aggregated usage information with us (#4108)
* Initial commit of BeaconConsent component

* Add comment about being able to change setting

* Use <Text> correctly

* Final version of consent screen

* Show beacon consent message on homepage only if it wasn't enabled already.

* Add consent setting to organization settings screen.

* Add support for custom message in OrgSetting.save.

* Implmenet consent saving.

* If consent given, send extra data

* Add HelpTrigger

* Make CodeClimate happy

* Wrap everything with DynamicComponent
2019-10-27 13:42:37 +02:00
Levko Kravets
7bf4219e58 Counter Editor: move components to own files (#4138) 2019-10-27 13:42:37 +02:00
Gabriel Dutra
fe477aa855 Add jsconfig settings with '@' webpack alias (#4135) 2019-10-27 13:42:37 +02:00
Levko Kravets
da09de6def Migrate Chart visualization to React Part 1: Renderer (#4130)
* Migrate Chart visualization: Renderer

* Refine PlotlyChart component; move stylesheets to visualization's folder

* Migrate Custom JS Chart to React

* Cleanup
2019-10-27 13:42:37 +02:00
Arik Fraimovich
f252821400 Remove duplicate messages method (#4131) 2019-10-27 13:42:37 +02:00
Levko Kravets
2cdc88293d Alerts: Add more condition comparison options (#4134)
* getredash/redash#4132 Add more condition comparison options

* Add arguments to fallback lambda
2019-10-27 13:42:37 +02:00
Levko Kravets
d2d78e7676 Allow the user to decide how to handle null values in charts (#4071)
* getredash/redash#2629 Refactor Chart visualization, add option for handling NULL values (keep/convert to 0.0)

* Handle null values in line/area stacking code; some cleanup

* Handle edge case: line/area stacking when last value of one of series is missing

* Mjnor update to line/area stacking code

* Fix line/area normalize to percents feature

* Unit tests

* Refine tests; add tests for prepareLayout function

* Tests for prepareData (heatmap) function

* Tests for prepareData (pie) function

* Tests for prepareData (bar, line, area) function

* Tests for prepareData (scatter, bubble) function

* Tests for prepareData (box) function

* Remove unused file
2019-10-27 13:42:37 +02:00
Ran Byron
c74ece4dda Decrease size of widget pagination (#4120)
* Added tests

* Perhaps this would trigger percy

* Decrease size of widget pagination

* Removed unused attr

* Updated tests
2019-10-27 13:42:37 +02:00
Arik Fraimovich
4a74263522 Sync botocor eversions across requirements files. (#4128) 2019-10-27 13:42:37 +02:00
Levko Kravets
4edfd23772 Migrate Counter visualization to React (#4106)
* Migrate Counter to React: Renderer

* Migrate Counter to React: Editor

* Cleanup

* Review and fix rows indexing algorithm

* Counter not properly scaled in editor

* Fix wrong label for/input id pair

* Tests

* Tests

* Fix vendor prefixes

* Remove unnecessary useEffect dependencies

* Update tests

* Fix Percy snapshot names
2019-10-27 13:42:37 +02:00
Arik Fraimovich
c9b3c95464 Upgrade Sentry-SDK and enable additional integratoins (#4127)
* Update sentry-sdk version

* Add additional Sentry integrations
2019-10-27 13:42:37 +02:00
Ran Byron
959822cca6 Widget table scroll-x visible (#4101)
* Table viz horizontal scroll made visible

* Added tests

* Fixed snapshot pre-condition

* Perhaps this would trigger percy
2019-10-27 13:42:37 +02:00
Justin Clift
4dea1d681f Update botocore, to get pass pip warning (#4122) 2019-10-27 13:42:37 +02:00
sphenlee
49b3dcaff7 hive_ds: show a user friendly error message when possible (#4121) 2019-10-27 13:42:37 +02:00
Gabriel Dutra
b59e210d90 Use ng-src for data source icons (#4123) 2019-10-27 13:42:37 +02:00
Ran Byron
10b57b6ee2 Fix number param value normlization (#4116) 2019-10-27 13:42:37 +02:00
Arik Fraimovich
cc21a32369 Move annotation logic into Query Runner (#4113)
* Code formatting

* Move annotation logic into query runner, so it can be overriden in the query runner.

* Add mixin to __all__

* Switch to flag instead of mixin

* Feature (Redshift): option to set query group for adhoc/scheduled queries  (#4114)

* Add scheduled status to query job metadata.

* Add: option to set query group for adhoc/scheduled Redshift queries

* Scheduled might not be set for already enqueued queries.
2019-10-27 13:42:37 +02:00
swfz
966b59906f Display data source icon in query editor (#4119) 2019-10-27 13:42:37 +02:00
Arik Fraimovich
a8440d32ab Add ability to use Ant's Table loading property when using ItemsTable (#4117) 2019-10-27 13:42:37 +02:00
Gabriel Dutra
e7765440fc Fix Dropdown parameter options appearing behind Dialog (#4109) 2019-10-27 13:42:36 +02:00
Arik Fraimovich
8af099b658 Fix: allow users with view only acces to use the queries in Query Results (#4112)
* Fix: allow users with view only acces to access the queries

* Add tests

* Update error message

* Update error message. Take 2
2019-10-27 13:42:36 +02:00
Ran Byron
7e9db06633 Fix widget bottom element alignment (#4110) 2019-10-27 13:42:36 +02:00
Omer Lachish
194d4e1750 Update badge in README.md to link to CircleCI (#4104)
* Update README.md

* Update README.md

* Update README.md

Co-Authored-By: Ran Byron <ranbena@gmail.com>

* Update README.md
2019-10-27 13:42:36 +02:00
shinsuke-nara
0207ba11a3 Migrate with SQL statements. (#4105) 2019-10-27 13:42:36 +02:00
Omer Lachish
61a80ad8cc Dashboard: when updating parameters, run only relevant queries (#3804)
* refresh only affected queries in dashboard when parameters are changed

* rename pendingParameters to updatedParameters

* select which widgets to update according to their mapping as a dashboard-level parameter

* use lodash's include
2019-10-27 13:42:36 +02:00
Sandeep Belagavi
cbfd994a28 [Qubole] - Adding support to process Quantum query types. (#4066)
* [Qubole] - Adding support to process Quantum query types.

Quantum is a serverless interactive service that offers
direct SQL access to user's data lake. Changes are made
to accept `quantum` query type from user which makes
`Cluster Label` as optional.

* -Making quantum as defult query.
-Dictionary safe access to connection parmeters

* keeping pep8 standards

* Maintainig pep8 std

* Use latest version of qds-sdk

* Use qds-sdk v1.13.0

* Use qds-sdk v1.12.0

* Use qds-sdk v1.13.0

* Updating SDK with verified version

* hive as default query type

* qds-sdk : Locking most recent release version

* qds-sdk : Locking recent release version

* falling back to original version of qds-sdk
2019-10-27 13:42:36 +02:00
Gleb Lesnikov
21ac9e8a97 [Data Sources] Add: Azure Data Explorer (Kusto) query runner (#4091)
* [Data Sources] Add: Azure Data Explorer (Kusto) query runner

* CodeClimate fixes

* Remove TODO

* Fixed configuration properties names for Azure Kusto

* Azure Kusto: get_schema in one query

* azure-kusto-data update to 0.0.32

* Add Kusto to the default query runners list
2019-10-27 13:42:36 +02:00
Arik Fraimovich
d53d05cfb9 Make sure we always pass a list to _get_column_lists (#4095)
(some data sources might return None as the columns list)
2019-10-27 13:42:36 +02:00
Ran Byron
ee85923b14 Removed redash-newstyle.less (#4017) 2019-10-27 13:42:36 +02:00
Arik Fraimovich
4866be60de Fix: MySQL connections without SSL are failing (#4090)
* Move connection logic into a single method & make sure not to pass ssl value if not used.

* Remove wildcard import and format file.
2019-10-27 13:42:36 +02:00
Christian Clauss
56d444b1a5 Add more flake8 tests and fail build if any test fails (#4055)
* Add more flake8 tests and fail build if any test fails

Run all flake8 E9xx + F63x + F7xx + F82x tests.

* long = long in Python 2
2019-10-27 13:42:36 +02:00
Gabriel Dutra
6b39437cdb Migrate Parameters component to React (#4006)
* Start Parameters Migration

* Add dirtyCount

* Use workaround with setState

* Apply Changes

* Add EditSettingsDialog

* Add Cmd/Ctrl + Enter behavior

* Remove isApplying

* Delete Angular version of parameters

* Update tests

* Remove angular stuff

* Update jest

* Drag placeholder

* Update events

* Use old button styling and move css

* Reviewing code

* Add parameter rearrange test

* Add Parameter Settings title change test

* Update Parameter Settings button styling

* Move parameter url logic back to Parameters

* Disable url update when query is new

* Styling changes (#4019)

* Ran's title width styling

* Update drag test

* Improve sizing for Number inputs

Co-Authored-By: Ran Byron <ranbena@gmail.com>

* Fix issue with dragged parameter wrapping

Co-Authored-By: Ran Byron <ranbena@gmail.com>

* Don't reevaluate dirtyParamCount

* Allow multiple values :)

* Fix parameter alignments

* Fix Select width on search

* Update client/app/components/Parameters.less

Co-Authored-By: Ran Byron <ranbena@gmail.com>

* Humanize param.name

* Make sure angular updates Execute disabled status
2019-10-27 13:42:36 +02:00
Vladimir Ponarevsky
b426e4fdc4 Fix clickhouse password leak (#4078)
* Fix clickhouse password leak

* Fix after review
2019-08-18 11:05:41 +03:00
Arik Fraimovich
e5e926bac5 Pin kombu version (#4075)
kombu is a dependency of Celery and usually we let them declare the version, but their version is pinned and the most recent release (4.6.4) has a severe regression where workers stop responding to inspect commands.
2019-08-16 19:27:24 +03:00
Arik Fraimovich
24d68008fa Format target value as a number with reasonable default (#4073) 2019-08-15 15:54:32 +03:00
Arik Fraimovich
0e90b89acc ParameterizedQuery: handle the case where a value is null (#4072) 2019-08-15 15:18:40 +03:00
Jannis Leidel
2c2f241671 Require a more up-to-date version of importlib-metadata. (#4069) 2019-08-15 11:50:27 +03:00
Jakdaw
d49514abe9 When we fork a query, make sure we create the new visualizations in the same order as per the source query (#4067) 2019-08-14 11:08:56 +03:00
Arik Fraimovich
934a145ced Switch to mysqlclient from Python-MySQL (#4061) 2019-08-14 10:11:53 +03:00
Omer Lachish
f7c70c2b91 Add parameter dialog doesn't work when query has selected text (#4032)
* debounce updateQuery to prevent pasting parameters over selected texts failing parseQuery (see #4032)

* drop defer
2019-08-14 07:47:34 +03:00
The Alchemist
69ba165565 [Data Sources] Initial commit for adding Dgraph support (#3987)
* Initial commit for adding Dgraph support

* Made suggestions from https://codeclimate.com/github/getredash/redash/pull/3964

* feedback from @arikfr

* added logo for Dgraph from Twitter

* Better conversion of Dgraph JSON to Redash's internal JSON

* made recommendations from @arikfr

* removed unused function
2019-08-13 13:14:37 +03:00
Jannis Leidel
7b5696dc75 Fix loading of periodic tasks and clean up extension loading. (#4064)
* Fix loading of periodic tasks and clean up extension loading.

This does a few things:

- add tests for extension loading
- refactor the extension and periodic task loading
- better handle assertions raised by extensions (e.g. when an extension tries to override an already registered view)
- attach exception traceback to error log during loading for improved debugging

* Use site.addsitedir instead of calling pip.

* Use sys.path instead of site.addsitedir and also the setup.py egg_info command.
2019-08-13 13:11:59 +03:00
Gabriel Dutra
4698408a08 Cypress: Fix cy.clock not freezing time (#4060) 2019-08-13 07:08:59 -03:00
Ievgen Aleinikov
be142d60df Add assume role as a credential source for AWS Athena Query runner (#4028)
* allowing to specify a custom work group for AWS Athena queries

* Fixing title + adding correct position in the UI

* Adding assume role configuration to Athena query runner.

* removing extra blank lines

* fixes based on comments to the PR
2019-08-12 16:45:56 +03:00
Arik Fraimovich
aceea6516f Change the required Docker Compose version to 3.2 (#4059)
With the default Docker installed from sources on Ubuntu 19.04 it failed starting the project when asking for Compose version 3.7, but everything worked fine with 3.2.
2019-08-12 13:26:29 +03:00
Arik Fraimovich
685b53672e Prevent CSP violations by not having script URLs (#4062)
* Fix: remove inline script to avoid CSP violation

Closes #4039.

* Restore eslint rule that prevents javascript href attributes.

* Remove all inline script links.
2019-08-12 13:25:07 +03:00
Arik Fraimovich
7dd62ef948 Add option to control whether to format target value. (#4063) 2019-08-12 13:24:11 +03:00
Evghenii Goncearov
7c2acc34c9 Dont send password reset link to disabled users (#2631)
* Dont send password reset link to disabled users

* Update email subject

* Update blocked email text.

* Update blocked email text (plain text version).

* Remove debug print.
2019-08-11 17:29:26 +03:00
Arik Fraimovich
c5a90876f3 Add Cassandra to the list of default enabled query runners (#4058) 2019-08-11 17:17:57 +03:00
Takuya Arita
8abaf89394 Add tag management commands (#3168) 2019-08-11 16:30:48 +03:00
PengYuan Lai
aa2bd0042e check float if scale > 0 in snowflake query result (#3876) 2019-08-11 16:21:57 +03:00
Yoshiken
a7b14bfb9a Fix according to pycodestyle format (#4011)
* Fix W292 no newline at end of file

* Fix extra whitespace

* Fix E305 expected 2 blank lines after class or function definition

* Fix W391 blank line at end of file

* Fix E231 missing whitespace after

* Fix E303 too many blank lines

* Fix E302 expected 2 blank lines

* Fix E128 continuation line under-indented for visual indent
2019-08-11 16:09:04 +03:00
Oluwafemi Sule
4e5f55a4b7 Align content vertically in restricted widget type (#4056) 2019-08-11 15:28:46 +03:00
Omer Lachish
76fbe858ba refresh_queries requires Request Context (#4045)
* avoid using 'abort' in parameterized query - raise an exception instead

* when facing invalid parameters or detached dropdown queries - continue to refresh the rest of the outdated queries

* test that dropdown queries detached from data source raise an exception when fetch values is attempted

* test that queries with invalid parameters arent refreshed

* test that queries with dropdown query parameters which are detached from the data source are skipped

* fix stale test double name

* newlines. newlines everywhere.

* pass org into dropdown_values

* pass in org in every ParameterizedQuery usage

* Update redash/tasks/queries.py

Co-Authored-By: Arik Fraimovich <arik@arikfr.com>

* reduce refresh_queries log noise

* track failure count for queries that failed to apply parameters, and also notify the failures

* Update redash/tasks/queries.py

Co-Authored-By: Arik Fraimovich <arik@arikfr.com>

* newlines. newlines everywhere.
2019-08-09 15:26:31 +03:00
Omer Lachish
cf7aef1e16 Make sure there is an event for any query execution (#4051)
* move event recording for query executions inside run_query

* include indication of cache hit or miss inside execute_query events
2019-08-09 15:24:17 +03:00
Jannis Leidel
77625b2a13 Remove duplicate base_url function. (#4043) 2019-08-08 10:44:44 +03:00
Omer Lachish
c4dcf01b3c avoid variable shadowing (#4050) 2019-08-07 22:43:04 +03:00
Ran Byron
a167c590b6 Added arrow to multi-select component (#4044) 2019-08-06 16:46:53 +03:00
Gabriel Dutra
8e23f93433 Allow dynamic values dropdown to scroll with the page (#4040) 2019-08-06 08:55:25 -03:00
Levko Kravets
e41d40bbe0 getredash/redash#4036 Visualisation editor crashes out when changing datetype to non-HTML text (#4037) 2019-08-05 12:37:21 +03:00
Gabriel Dutra
6fc4d5b551 Focus DatePicker after selecting dynamic values (#4033) 2019-08-04 22:24:33 -03:00
Gabriel Dutra
f0576a3623 Support multi-select in parameters (#3952)
* Allow multiple values for enum parameter

* Allow multi-select for Query dropdown parameters

* CR + make sure list values are allowed

* Add prefix, suffix and separator

* Rename multipleValues and cast options as strings

* Replicate serialization logic on frontend

* Add Quote Option Select

* Make sure it's enum or query before join

* Add a couple of tests

* Add help to quote option

* Add min-width and normalize empty array

* Improve behavior when changing parameter settings
- Set parameter value again to pass through checks
- Add setValue check for multi values

* Validate enum values on setValue + CodeClimate

* Ran wording suggestions

* Updates after Apply Changes

* Fix failing Cypress tests

* Make sure enumOptions exists before split

* Improve propTypes for QueyBasedParameterInput

Co-Authored-By: Ran Byron <ranbena@gmail.com>

* CR

* Cypress: Test for multi-select Enum

* Fix multi-selection Cypress spec

* Update Refresh Schedule
2019-08-04 15:47:30 +03:00
Levko Kravets
9eabf89771 getredash/redash#4031 Counter visualization: formatting not applied to target value (#4035) 2019-08-04 15:22:53 +03:00
Arik Fraimovich
11cc274c1c Update Snowflake connector version to latest (#4029) 2019-08-04 08:55:02 +03:00
Ran Byron
8ad08a566a Revert "Revoked widget refresh button spinners" (#4027)
This reverts commit ab5494a8fd.
2019-08-01 08:23:17 +03:00
Levko Kravets
ef31d0d768 Fix: don't update dashboard's version when adding a widget (#4026) 2019-07-31 22:28:12 +03:00
Levko Kravets
4640c33387 Bug fix: error when trying to collect dashboard-level filters for a textbox widget (#4024) 2019-07-31 18:03:40 +03:00
Levko Kravets
9b290913a6 Migrate Table visualization to React Part 1: Renderer (#3963) 2019-07-31 17:33:33 +03:00
Ran Byron
db89c4f7bc Turned off max asset size warning (#4023) 2019-07-31 11:34:52 +03:00
Gabriel Dutra
eae1fb7d73 Force readonly inputs click (#4016) 2019-07-30 11:16:34 +03:00
Arik Fraimovich
4f742aeaac Fix: support for unicode in DynamoDB queries (#4015) 2019-07-30 11:14:57 +03:00
Ran Byron
5ddad862be Updated timeago strings (#4012)
* Updated timeago strings

* Moved moment config to app/config
2019-07-29 18:03:59 +03:00
Ran Byron
6f811f163a Added widget header refresh indicator (#3970) 2019-07-29 16:43:44 +03:00
Omer Lachish
7fb33e3ebb Failed Scheduled Queries Report (#3798)
* initial work on e-mail report for failed queries

* send failure report only for scheduled queries and not for adhoc queries

* add setting to determine if to send failure reports

* add setting to determine interval of aggregated e-mail report

* html templating of scheduled query failure report

* break line

* support timeouts for failure reports

* aggregate errors within message and warn if approaching threshold

* handle errors in QueryExecutor.run instead of on_failure

* move failure report to its own module

* indicate that failure count is since last report

* copy changes

* format with <code>

* styling, copy and add a link to the query instead of the query text

* separate reports with <hr>

* switch to UTC

* move <h2> to actual e-mail subject

* add explicit message for SoftTimeLimitExceeded

* fix test to use soft time limits

* default query failure threshold to 100

* use base_url from utils

* newlines. newlines everywhere.

* remove redundant import

* apply new design for failure report

* use jinja to format the failure report

* don't show comment block if no comment is provided

* don't send emails if, for some reason, there are no available errors

* subtract 1 from failure count, because the first one is represented by 'Last failed'

* don't show '+X more failures' if there's only one

* extract subject to variable

* format as text, while we're at it

* allow scrolling for long exception messages

* test that e-mails are scheduled only  when beneath limit

* test for indicating when approaching report limits + refactor

* test that failures are aggregated

* test that report counts per query and reason

* test that the latest failure occurence is reported

* force sending reports for testing purposes

* Update redash/templates/emails/failures.html

Co-Authored-By: Ran Byron <ranbena@gmail.com>

* Update redash/templates/emails/failures.html

Co-Authored-By: Ran Byron <ranbena@gmail.com>

* Update redash/tasks/failure_report.py

* add org setting for email reports

* remove logo from failure report email

* correctly use the organization setting for sending failure reports

* use user id as key for failure reports data structure

* Update redash/tasks/failure_report.py

Co-Authored-By: Arik Fraimovich <arik@arikfr.com>

* build comments while creating context for e-mail templates

* figure out the base url when creating the e-mail

* no need to expire pending failure report keys as they are deleted anyway when sent

* a couple of CodeClimate changes

* refactor key creationg to a single location

* refactor tests to send e-mail from a single function

* use beat to schedule a periodic send_aggregated_errors task instead of using countdown per email

* remove pending key as it is no longer required when a periodic task picks up the reports to send

* a really important blank line. REALLY important.

* Revert "a really important blank line. REALLY important."

This reverts commit c7d8ed8972.

* a really important blank line. REALLY important. It is the best blank line.

* don't send failure emails to disabled users
2019-07-28 12:40:54 +03:00
Omer Lachish
f165168860 recycle gunicorn workers (#4013) 2019-07-28 11:39:14 +03:00
Gabriel Dutra
86b0608fde Fix Apply Changes is lost when query is edited (#4010)
Co-Authored-By: Ran Byron <ranbena@gmail.com>
2019-07-27 19:05:49 -03:00
Gabriel Dutra
cd4daf8823 Add Dynamic Values to Date and Date Range Parameters (#3904)
* Draft for Date Dynamic values

* Use value with prefix instead of specific attr

* Fix not possible to select static value

* Update antd version

* Cleanup and DateRangeParameter

* Dynamic DateTimeRange

* Add Dynamic options to Date Parameters

* UI refinements

* Add getDynamicValue function

* Add 'This' options and prevent text clipping

* Make allowClear available

* Update ScheduleDialog snapshot

* Add some protections and separate Date/DateRange

* Accept null values on date or daterange parameters

* Handle undefined values on Moment propType

* Move export to end of files

* Remove Today/Now option

* Update with Apply Changes

* Show name instead of value for dynamic values

* Add comment about supporting useCurrentDateTime

* Cypress Tests: Date Parameters

* Cypress Tests: Date Range Parameters

* Don't put null params in the url

* Add workaround comments to Cypress tests

Co-Authored-By: Ran Byron <ranbena@gmail.com>

* Fix Dynamic Value as default for global parameters

* Update Back to Static Value

* Add isValid to value on Date and DateRange inputs

* CR suggestions

* Fix Back to Static Value for Dates

* Update Dynamic Value Styling

* Fix failing Date tests

* Fix selectedDynamicValue

* Parameter spec: Remove date range clickThrough

* Add transition

* Fix failing Cypress tests

* Back with 'width: auto'

* Check value is valid on Back to Static value

* CR

* Update Date Range width
2019-07-26 22:40:13 +03:00
Gabriel Dutra
78cae474e0 Cypress: Specify widgets position on sharing spec (#4009) 2019-07-26 13:15:39 -03:00
Naoyuki Kataoka
c518c7a4bc Modified PagerDuty destination to avoid an error for multi-byte characters (#4008) 2019-07-24 09:06:25 +03:00
Gabriel Dutra
8c2f51d09d Percy: Fix shared dashboard inconsistent snapshots (#4002) 2019-07-23 11:55:24 -03:00
Gabriel Dutra
6f6c68bd79 Cypress: Separate dashboard spec (#4003) 2019-07-22 11:09:08 -03:00
Ran Byron
64f274f58e Disable execute when params are dirty (#4001)
* Disable execute when params dirty

* Removed special apply handling for query page

* Updated tests
2019-07-22 12:13:34 +03:00
Omer Lachish
dd89bd885f Add "deprecated" flag to query runners (and alert destinations) (#3972)
* add a deprecated flag to query runners and show only non-deprecated query runners when adding a new data source

* add a deprecated flag to alert destinations and show only non-deprecated alert destinations when adding a new alert destination

* add a deprecated() decorator for a more succint way to deprecate

* deprecate URL query runner and HipChat alert destination

* use class properties instead of class methods for deprecation

* I <3 newlines
2019-07-22 10:36:31 +03:00
Ran Byron
b2295197cf Added publish notification to query rename (#3998) 2019-07-21 15:01:08 +03:00
Omer Lachish
ea0e411053 Return unsafe sharing error from backend (#3990)
* return message explaining unsafe sharing

* use backend-generated message for public dashboards

* use backend-generated message for embeds

* Update redash/handlers/query_results.py

Co-Authored-By: Arik Fraimovich <arik@arikfr.com>

* refactor simple (non-interpolated) query result handler error messages to a single location

* use error_messages to test out unsafe error messages (along with a couple of others)

* Update redash/handlers/query_results.py

Co-Authored-By: Ran Byron <ranbena@gmail.com>

* Update redash/handlers/query_results.py

Co-Authored-By: Arik Fraimovich <arik@arikfr.com>
2019-07-21 09:21:45 +03:00
Arik Fraimovich
9bdb3412a5 Move query runners/destinations import from redash.app to redash. (#3993)
* Move query runners/destinations import from redash.app to redash.

* Add missing argument
2019-07-21 09:05:29 +03:00
Fumiya Karasawa
ad4a760545 Search dropdown parameters (#3796) 2019-07-20 16:07:03 +03:00
Omer Lachish
c1f4147807 Avoid committing it.only (#3995)
* remove it.only, left by mistake

* use no-only-tests

* 'off' should be used instead of 'none'

* Dedup jest/only rule

* always error for .only
2019-07-19 18:30:38 +03:00
Ran Byron
c054ae8be0 Fixed filter style issue (#3996) 2019-07-18 13:49:37 +03:00
Omer Lachish
d1edd3d068 Query Result API response shouldn't include query information for non authenticated users (#3985)
* avoid catching errors on text widgets' load(), as they don't have a visualization and therefore do not return any promise

* throw error when failing to load widgets on public dashboards - in case something needs to be done with it at a later time, and it's the right thing to do anyway

* use Promise.resolve instead of checking for undefined

* call serialize_query_result instead of directly calling to_dict

* filter unneeded query result fields for unauthenticated users

* test for serialization filtering

* lint

* use project instead of list comprehension
2019-07-18 12:12:49 +03:00
Arik Fraimovich
4989bfae60 Remove custom Redis connection code in favor of redis.from_url (#3992) 2019-07-18 12:03:52 +03:00
Gabriel Dutra
f20a020003 Use AceEditor for Query Snippets (#3973)
Co-Authored-By: Ran Byron <ranbena@gmail.com>
2019-07-17 13:47:31 -03:00
Ran Byron
01da8c158a Parameter “Apply Changes” button (#3907) 2019-07-17 17:17:39 +03:00
Omer Lachish
c83e40b047 Celery doesn't auto reload in development (#3898)
* pick up *.py file changes and restart scheduler

* only watch /redash in order to avoid reloading on other file changes (e.g. tests)

* add dev_scheduler entrypoint

* use exec

* Update bin/docker-entrypoint

* rename dev_scheduler to dev_worker

* use same defaults as worker
2019-07-17 10:38:56 +03:00
Ran Byron
c3cc65a21d Viz embed logo alignment (#3956) 2019-07-16 11:37:31 +03:00
Omer Lachish
5929139ab8 A couple of parameters-on-public-dashboards loose ends (#3988)
* avoid catching errors on text widgets' load(), as they don't have a visualization and therefore do not return any promise

* throw error when failing to load widgets on public dashboards - in case something needs to be done with it at a later time, and it's the right thing to do anyway

* use Promise.resolve instead of checking for undefined
2019-07-16 10:48:37 +03:00
Ran Byron
66794acd1f Added loading indicator to public dashboard (#3984) 2019-07-16 10:31:19 +03:00
Arik Fraimovich
bce0832e48 Show error in case of failing to load a dashboard (#3983) 2019-07-15 22:13:46 +03:00
Yuri Grishaev
9f006997a0 mattermost needs whitespace to use h4 heading (#3981)
#### Even Smaller Heading - good
####Even Smaller Heading - bad
2019-07-15 21:13:17 +03:00
Omer Lachish
51d8131db5 Allow Parameters on Public Dashboards (#3659)
* change has_access and require_access signatures to work with the objects that require access, instead of their groups

* use the textless endpoint (/api/queries/:id/results) for pristine
queriest

* Revert "use the textless endpoint (/api/queries/:id/results) for pristine"

This reverts commit cd2cee7738.

* go to textless /api/queries/:id/results by default

* change `run_query`'s signature to accept a ParameterizedQuery instead of
constructing it inside

* raise HTTP 400 when receiving invalid parameter values. Fixes #3394

* enqueue jobs for ApiUsers

* rename `id` to `user_id`

* support executing queries using Query api_keys by instantiating an ApiUser that would be able to execute the specific query

* show deprecation messages for ALLOW_PARAMETERS_IN_EMBEDS. Also, move
other message (email not verified) to use the same mechanism

* add link to forum message regarding embed deprecation

* change API to /api/queries/:id/dropdowns/:dropdown_id

* split to 2 different dropdown endpoints and implement the second

* add test cases for /api/queries/:id/dropdowns/:id

* use new /dropdowns endpoint in frontend

* first e2e test for sharing embeds

* Pleasing the CodeClimate overlords

* All glory to CodeClimate

* remove residues from bad rebase

* add query id and data source id to serialized public dashboards

* add global parameters directive to public dashboards page

* allow access to a query by the api_key of the dashboard which includes
it

* rename `object` to `obj`

* simplify permission tests once `has_access` accepts groups

* support global parameters for public dashboards

* change has_access and require_access signatures to work with the objects that require access, instead of their groups

* rename `object` to `obj`

* simplify permission tests once `has_access` accepts groups

* no need to log `is_api_key`

* send parameters to public dashboard page

* allow access to a query by the api_key of the dashboard which includes it

* disable sharing if dashboard is associated with unsafe queries

* remove cypress test added in the wrong place due to a faulty rebase

* add support for clicking buttons in cy.clickThrough

* Cypress test which verifies that dashboards with safe queries can be shared

* Cypress test which verifies that dashboards with unsafe queries can't be shared

* remove duplicate tests

* use this.enabled and negate when needed

* remove stale comment

* add another Cypress test to verify that unauthenticated users have access to public dashboards with parameters

* obviously, I commit 'only' the first time I use it

* search for query access by query id and not api_key

* no need to fetch latest query data as it is loaded by frontend from the textless endpoint

* test that queries associated with dashboards are accessible when supplying the dashboard api_key

* propagate `isDirty` down to `QueryBasedParameterInput`

* go to /api/:id/dropdown while editing a query, since dropdown queries might still not be associated with the parent. see #3711

* show helpful error message if dropdown values cannot be fetched

* use backticks instead of line concatenation

* remove requirement to have direct access to dropdown query in order validate it. parent query association checks are sufficient

* remove isDirty-based implementation and allow dropdown queries through nested ACL even if they aren't associated yet (given that the user has _direct_ access to the dropdown query)

* fix tests to cover all cases for /api/queries/:id/dropdowns/:id

* fix indentation

* require access to the query, not the data source

* resolve dashboard user by query id

* apply new copy to Cypress tests

* if only something would have prevented me from commiting an 'only' call 🤔

* very important handling of whitespace

* respond to parameter's Apply button

* text widgets are safe for sharing

* remove redundant event

* add a safety check that object has dashboard_api_keys before calling it

* supply a parameter value for text parameters to have it show up

* add parameter values for date and datetime

* use the current year and month to avoid pagination

* use Cypress.moment() instead of preinstalled moment()

* explicitly create parameters

* refresh query data if a  querystring parameter is provided

* avoid sending a data_source_id - it's only relevant to unsaved queries, since a saved query's data_source is available in the backend

* remove empty query text workaround

* provide default value to parameter

* add a few more dashboard sharing specs

* lint

* wait for DynamicTable to appear to reveal that actual results are displaying

* override error message for unsafely shared widgets
2019-07-15 15:09:30 +03:00
Arik Fraimovich
c793b5dd11 Remove explicit kombu dependency (#3978)
We used an explicit kombu dependency to target the correct Redis version, but current version of Celery supposed to use it by default.
2019-07-14 08:47:15 +03:00
Levko Kravets
4e9da3f116 [Bug fix] Plotly legend overlaps plot on small screens (when legend clicked) (#3976) 2019-07-13 18:27:53 +03:00
Arik Fraimovich
15a8eecdde JSON Data Source (#3805)
* WIP: JSON Data Source

* Add JSON data source to default list
2019-07-11 14:23:38 +03:00
k-tomoyasu
a8ff2500be Build custom alert message (#3137)
* build custom alert message

* fit button color tone

* pass existing test

* fix typos

* follow code style

* add webhook alert description and avoid key error

* refactor: create alert template module

* follow code style

* use es6 class, fix template display

* use alerts.options, use mustache

* fix email description

* alert custom subject

* add alert state to template context, sanitized preview

* remove console.log 🙇

* chatwork custom_subject

* add alert custom message. pagerduty, mattermost, hangoutschat

* Pass custom subject in webhook destination

* Add log message when checking alert.

* Add feature flag for extra alert options.
2019-07-11 13:23:06 +03:00
Ran Byron
7bf84e856c Workaround fixes for datepicker display bug in Cypress tests (#3967) 2019-07-10 17:47:46 +03:00
Levko Kravets
5149bf67ca [Bug fix] Archiving a dashboard cause widgets to show reload spinner forever (#3968) 2019-07-10 13:35:15 +03:00
Arik Fraimovich
93449db325 Improvements to Query Result serialization code (#3960)
* Fix: allow serializing empty or bad dates

* Improve date serialization performance

* Remove duplicate assertion.
2019-07-10 10:51:34 +03:00
Arik Fraimovich
df57d22e81 Add explicit route for dashboards to allow embedding in iframes. (#3957)
* Add explicit route for dashboards to allow embedding in iframes.

* Add missing blank lines
2019-07-10 10:25:08 +03:00
Gabriel Dutra
de0a44ee85 Migrate Query Snippets to React (#3627) 2019-07-09 09:27:39 -03:00
Tomoki Sekiyama
261062d491 Support multi-byte search for query names and descriptions (#3908)
* Support multi-byte search for query names and descriptions

* add multi_byte_support_enabled option to organization settings

* add `ilike %...%` to query search conditions when the option is enabled

* Improve description for multi_byte_search_enabled option

Co-Authored-By: Arik Fraimovich <arik@arikfr.com>

* Remove tsvector from search when multi_byte_search_enabled

* Add a multi-byte search test case
2019-07-08 10:01:47 +03:00
Arik Fraimovich
1878e8bf90 Add additional Celery config options (#3961) 2019-07-08 08:32:18 +03:00
Arik Fraimovich
47fc8a942a Add options to hide different parts of embed UI (parameters, title, link to query) (#3955)
* Move closing tag to correct location

* Add options to hide elements in query embed UI

* Fix for headless top padding (#3959)
2019-07-07 14:34:00 +03:00
Arik Fraimovich
addecbdd8f Allow calling query results endpoint without parameters. (#3958)
* Allow calling query results endpoint without parameters.

* Fix: allow serializing empty or bad dates

* Revert "Fix: allow serializing empty or bad dates"

This reverts commit cc49319d9e.
2019-07-07 14:22:08 +03:00
Gabriel Dutra
baec5d56f5 Remove time from Date column in filters (#3953) 2019-07-05 18:13:12 -03:00
Levko Kravets
1f4325ba8d Migrate Box Plot visualization to React (#3948) 2019-07-04 22:25:09 +03:00
Arik Fraimovich
5e5b56ed6a Fix: render date/time values as strings and not epoch time (#3951) 2019-07-04 20:49:59 +03:00
Arik Fraimovich
45a3b72730 Update fsevents to v1.2.9 (#3950) 2019-07-04 15:43:43 +03:00
Levko Kravets
cc48de0d8f Migrate Word Cloud visualization to React (#3930) 2019-07-03 13:29:05 +03:00
Arik Fraimovich
300f3f6780 Fix: waiting tasks are not shown in admin view (#3942)
* Fix: waiting tasks are not shown properly

* Added a comment.
2019-07-03 11:09:42 +03:00
Omer Lachish
2e4a69cba4 Parameter spec fixes (#3932)
* supply a parameter value for text parameters to have it show up

* add parameter values for date and datetime

* use the current year and month to avoid pagination

* use Cypress.moment() instead of preinstalled moment()

* capture time before clicking on Now

* use now from input

* use now from input for another test
2019-07-01 09:56:55 +03:00
Arik Fraimovich
6748e9a15d Add option to hide Pivot Table totals (#3943)
* Add option to hide Pivot Table totals

* Simplify implementation using DEFAULT_OPTIONS.

* Flip hide pivot controls to show pivot controls

* Update client/app/visualizations/pivot/Editor.jsx

Co-Authored-By: Ran Byron <ranbena@gmail.com>
2019-06-30 15:43:18 +03:00
Arik Fraimovich
7ceb68a468 Visualization: details view (#3778)
* Details visualization

* Add PropTypes and guard against no rows
2019-06-30 15:34:02 +03:00
Levko Kravets
3c1d1e3d4e Explicitly mark default visualization (#3944) 2019-06-30 14:09:00 +03:00
Levko Kravets
92391e3cbc [Bug fix] Toggling Use Dashboard Level Filter cause widgets to show reload spinner forever (#3939) 2019-06-28 15:02:07 +03:00
Levko Kravets
17438002d7 [Bug fix] Adding widget from query page is broken (#3921) 2019-06-25 20:06:36 +03:00
Jianchao Yang
a00c5a8857 Dockerfile front end stage copies client side files only (#3924)
So that changing other files will not trigger the
very expensive rebuild process.
2019-06-23 11:33:52 +03:00
John Karahalis
a696fa55f3 Set unique class name for Query Control Dropdown (#3922)
This will help me target the Query Control Drodpwon in my extension.
2019-06-23 10:01:46 +03:00
Arik Fraimovich
27259b5abe Add support for int/float values in guess_type (#3913) 2019-06-20 08:55:31 +03:00
Ran Byron
9ee393ec75 Fix schedule dialog needless confirm saves (#3919) 2019-06-20 08:54:53 +03:00
Ran Byron
cfafa97218 Fixed boolean filter (#3915) 2019-06-19 14:54:27 +03:00
deecay
be580b24a5 Expose celery job timeout setting to env var (#3912)
* Expose celery job timeout to env

* Change variable name
2019-06-19 14:41:12 +03:00
Arik Fraimovich
a6960c5f19 Fix: time format option was wrong (#3916) 2019-06-19 14:37:02 +03:00
Arik Fraimovich
6dd321beeb Rockset: handle query errors (#3910) 2019-06-18 14:10:25 +03:00
Arik Fraimovich
27c64b42ac Add keyboard shortcut for format query (Ctrl/Cmd+Shift+F) (#3911)
* Add keyboard shortcut for format query

* Added to button tooltip
2019-06-18 14:10:05 +03:00
deecay
99bf6d122c Custom Map Markers (#3840) 2019-06-18 09:50:09 +03:00
Ran Byron
d617f57f7d Increase celery job timeout (#3903) 2019-06-17 08:35:02 +03:00
Guy Cohen
21a27ee0b1 Fix OverflowError on celery worker (#3899) 2019-06-16 11:34:29 +03:00
YOSHIDA Katsuhiko
ac293c7f92 Add alert deletion confirmation dialog (#3902) 2019-06-15 14:28:55 +03:00
YOSHIDA Katsuhiko
8e38dcd244 Support regenerating Query API Key (#3764)
* Add regenerate function of query's API Key

* Add regenerate API Key button

* Add regenerate Query API Key tests

* Fix too long line

* Replace  with this

* Return a simple version query

* Update only API Key

* Update API Key via query
2019-06-12 13:09:21 +03:00
Aidarbek Suleimenov
2bab144107 Celery task to clear schedule was added (#3801)
* Celery task to clear schedule was added

* fix formating

* empty_schedules task was put in separate task

* worker interval changed, new tests added

* past artifact deleted

* test queries moved to right class, lambda was used to filter data

* unnecessary changes eliminated

* more unnecessary files deleted

* line shortened

* Line shortened more

* codeclimate changes

* Unused test deleted, logs added
2019-06-12 13:07:15 +03:00
Mike Nason
4e0a251034 Add support ssl connections to redis (#3848)
* Add support ssl connections to redis

* Fix line length

* Update redash/__init__.py w suggestion

Co-Authored-By: Omer Lachish <omer@rauchy.net>

* Cleanup init after suggestion

* Move redis SSL config to settings

* Do not pass celery SSL config unless necessary

* Fix typo
2019-06-12 13:04:34 +03:00
Levko Kravets
7a9f4b07e0 Force a Choose account step for Google OAuth (#3884) 2019-06-12 11:48:25 +03:00
Arik Fraimovich
1630cbb904 Google Sheets: friendlier error message in case of an API error and more reliable test connection (#3883)
* Google Sheets: friendlier error message in case of an APIError and more reliable test connection.

* Pleasing the pep8 gods
2019-06-12 11:46:59 +03:00
Levko Kravets
f8d05dda9f getredash/redash#3879 Plotly legent overlaps plot on small screens (FF only) (#3882) 2019-06-12 11:46:34 +03:00
Omer Lachish
2af8b39d21 Authorize according to API key (if given) over cookies (#3877)
* remove legacy session identifier support

* remove redundant test

* redirect to login to support any invalid session identifiers

* be more specific with caught errors

* use authorization according to api_key (if provided) over session
2019-06-12 11:45:28 +03:00
dependabot[bot]
3faed0fdfe Bump pyopenssl from 16.2.0 to 17.5.0 (#3872)
Bumps [pyopenssl](https://github.com/pyca/pyopenssl) from 16.2.0 to 17.5.0.
- [Release notes](https://github.com/pyca/pyopenssl/releases)
- [Changelog](https://github.com/pyca/pyopenssl/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pyca/pyopenssl/compare/16.2.0...17.5.0)
2019-06-12 11:38:47 +03:00
dependabot[bot]
e45f49b86e Bump cryptography from 2.0.2 to 2.3 (#3870)
Bumps [cryptography](https://github.com/pyca/cryptography) from 2.0.2 to 2.3.
- [Release notes](https://github.com/pyca/cryptography/releases)
- [Changelog](https://github.com/pyca/cryptography/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/2.0.2...2.3)
2019-06-12 11:38:38 +03:00
Omer Lachish
e33ad3b164 Query Results: querying a column with a dictionary or array fails (#3887)
* flatten lists and dicts to json to be used with SQLite's json_extract functions

* add test that verifies that lists and dicts are saved

* add test that verifies that lists and dicts are saved
2019-06-11 17:41:15 +03:00
Omer Lachish
6605f62f3a add api_key to embed download urls (#3896) 2019-06-11 14:02:02 +03:00
Omer Lachish
ed2ac407ab Remove schema after deleting data source (#3894)
* remove schema from redis after deleting data sources

* switch to _pause_key to property
2019-06-10 22:39:26 +03:00
Ran Byron
dda75cce24 Drawer menu with recreated close button (#3889)
* Drawer menu with recreated close button

* Added “Open in new window” drawer menu button (#3890)
2019-06-09 12:21:53 +03:00
Omer Lachish
5b780ac460 Refresh Public Dashboards (#3881)
* remove legacy session identifier support

* remove redundant test

* redirect to login to support any invalid session identifiers

* be more specific with caught errors

* fix refresh for public dashboards
2019-06-06 11:07:12 +03:00
koooge
c0e8ef3000 Upgrade gspread 3.1.0 for supporting team drive (#3838)
* Upgrade gspread 3.1.0 for supporting team drive

Signed-off-by: koooge <koooooge@gmail.com>

* Revert "Upgrade gspread 3.1.0 for supporting team drive"

This reverts commit e53e8cb75b.

* Upgrade gspread 3.1.0 for supporting team drive

Signed-off-by: koooge <koooooge@gmail.com>

* Update Sheets query runner name
2019-06-06 11:02:08 +03:00
Gabriel Dutra
a82fd0cabc Cypress: Fix date parameters false positive (#3873) 2019-06-04 12:19:55 -03:00
Ran Byron
0e3e2eaf38 Restrict dynamic-table internal scroll only when pagination appears (#3875) 2019-06-04 09:11:19 -06:00
Omer Lachish
05f6ef0fb6 Remove legacy session identifier support (#3866)
* remove legacy session identifier support

* remove redundant test

* redirect to login to support any invalid session identifiers

* be more specific with caught errors
2019-06-03 22:18:24 +03:00
dependabot[bot]
e433efebc4 Bump flask from 0.11.1 to 0.12.3 (#3871)
* Bump flask from 0.11.1 to 0.12.3

Bumps [flask](https://github.com/pallets/flask) from 0.11.1 to 0.12.3.
- [Release notes](https://github.com/pallets/flask/releases)
- [Changelog](https://github.com/pallets/flask/blob/master/CHANGES.rst)
- [Commits](https://github.com/pallets/flask/compare/0.11.1...0.12.3)

* Bump to Flask 0.12.4 to fix an issue
2019-06-03 18:04:43 +03:00
Arik Fraimovich
a9588eac79 Update version to 8.0.0-beta. (#3869) 2019-06-02 14:37:16 +03:00
Arik Fraimovich
090b570a71 Rearrange README badges. 2019-06-02 14:01:09 +03:00
Arik Fraimovich
60b12e3121 Update PromiseRejectionError to show error message from API response (#3868)
* Update PromiseRejectionError to show error message from API response

* Update version to 8.0.0-beta.

* Revert "Update version to 8.0.0-beta."

This reverts commit c8fa74967f.
2019-06-02 11:47:26 +03:00
Arik Fraimovich
3f8c7333be Use the debian flavor of the redash/base image (#3240) 2019-06-02 11:42:19 +03:00
Arik Fraimovich
be8dec5f04 Add Collapse component (#3867) 2019-06-02 11:12:37 +03:00
Levko Kravets
10b3b50f3d getredash/redash#3862 Widget menu should not be available on public dashboards (#3863) 2019-06-02 11:00:10 +03:00
Levko Kravets
6f290ddfa1 Use more specific CSS for public dashboard page to avoid conflicts with other components (#3864) 2019-05-31 23:41:43 +03:00
Ran Byron
10b62ebe02 Beautify autoheight code a bit (#3865) 2019-05-31 14:09:29 -06:00
Ran Byron
04453409da Fix widget autoHeight related exception (#3858) 2019-05-31 05:57:26 -06:00
Gabriel Dutra
b27df216f4 Cypress tests for query parameters (#3810) 2019-05-30 10:01:44 -03:00
Levko Kravets
a0c76d777b getredash/redash#3851 Disable filter which does not have values (#3852) 2019-05-30 15:53:28 +03:00
Levko Kravets
2e96e2fb98 getredash/redash#3849 Fix initialization of dashboard-level parameters (#3853) 2019-05-30 15:51:58 +03:00
Gabriel Dutra
c2e31f040d Fix table isn't displayed with date filter (#3842) 2019-05-29 09:40:25 -03:00
Arik Fraimovich
816f4d912f Capitalize "elsewhere" (#3844) 2019-05-29 14:47:17 +03:00
Arik Fraimovich
9292ae8d3f CSV: correctly serialize booleans and dates. (#3841)
* CSV: correctly serialize booleans and dates.

Closes #3736, closes #2751.

* pep8 fixes

* Move column iteration to a helper function.

* Use elif, as types are mutually exclusive.

* Refactor parsing implementation.

* Move the csv generation fucntion
2019-05-29 10:45:29 +03:00
Levko Kravets
9480d89e4c [Feature] Migrate CreateDashboardDialog to React (#3826) 2019-05-27 23:12:52 +03:00
Ran Byron
5dff5b929c Unsupported browser redirect script outputted to file (#3832) 2019-05-27 13:18:04 -06:00
Gabriel Dutra
28e9740e2f Update Data Source Lifecycle Events (#3828) 2019-05-27 19:28:16 +03:00
Ran Byron
7679df63ba Fix for Mac browser’s scrollbar obscuring content (#3830) 2019-05-27 18:31:02 +03:00
Jannis Leidel
07c9530984 Decouple extensions from Flask app. (#3569)
* Decouple extensions from Flask app.

This separates the extension registry from the Flask app and also introduces a separate registry for preriodic tasks.

Fix #3466.

* Address review feedback.

* Update redash/extensions.py

Co-Authored-By: jezdez <jannis@leidel.info>

* Minor comment in requirements.

* Refactoring after getting feedback.

* Uncoupled bin/bundle-extensions from Flas app instance.

* Load bundles in bundle script and don’t rely on Flask.

* Upgraded to importlib-metadata 0.9.

* Add missing requirement.

* Fix TypeError.

* Added requirements for bundle_extension script.

* Install bundles requirement file correctly.

* Decouple bundle loading code from Redash.

* Install bundle requirements from requirements.txt.

* Use circleci/node for build-docker-image step, too.
2019-05-26 14:56:02 +03:00
Rueian
aecd0bf37a include bigquery_gce.png in db-logos (#3825) 2019-05-26 13:27:29 +03:00
Omer Lachish
4143bd3f20 when authenticated, the query shouldn't be sent over to the /jobs endpoint (#3831) 2019-05-26 12:19:32 +03:00
Arik Fraimovich
020dc35faf Create SECURITY.md (#3823)
* Create SECURITY.md

* Update SECURITY.md

Co-Authored-By: Gabriel Dutra <nesk.frz@gmail.com>
2019-05-26 09:32:41 +03:00
Gabriel Dutra
d7b03bac02 Add CircleCI env vars to Cypress docker (#3827) 2019-05-23 22:33:23 +03:00
Omer Lachish
29875e66d4 Plug custom Celery tasks via dynamic settings (#3819)
* plug custom celery tasks via dynamic settings

* an extra blank line
2019-05-22 11:37:18 +03:00
taminif
d97ce15837 refactor format semi-colon (#3812) 2019-05-22 11:10:16 +03:00
Gabriel Dutra
b263bb7077 [Bug fix] Fix "Now" in DateTime parameter not working (#3808) 2019-05-16 18:42:48 +03:00
Ran Byron
606cf12e74 Dashboard grid React migration #1 (#3722)
* Dashboard grid React migration

* Updated tests

* Fixes comments

* One col layout

* Tests unskipped

* Test fixes

* Test fix

* AutoHeight feature

* Kebab-cased

* Get rid of lazyInjector

* Replace react-grid-layout with patched fork to fix performance issues

* Fix issue with initial layout when page has a scrollbar

* Decrease polling interval (500ms is too slow)

* Rename file to match it's contents

* Added some notes and very minor fixes

* Fix Remove widget button (should be visible only in editing mode); fix widget actions menu

* Fixed missing grid markings

* Enhanced resize handle

* Updated placeholder color

* Render DashboardGrid only when dashboard is loaded
2019-05-16 15:43:46 +03:00
Levko Kravets
4508975749 [Bug fix] Plotly modebar appears above modals (#3799) 2019-05-15 10:43:34 +03:00
Gabriel Dutra
c76955be28 Refresh query when parameters update (#3737)
* Add touch state to parameters and autoupdate query

* Use values change event instead of $watch

* Remove getQueryResultDebounced

* Add Apply button

* Remove Input Number spinners for Parameters

* Make Apply Button optional

* Update share_embed_spec

* Change debounce to the Parameters component

* Remove unnecessary click on Execute query

* Add apply button to the remaining places

* Update dashboard_spec

* Use onKeyUp for InputNumber

* Simplify onParametersValuesChanged

* Update DateTime onChange function

* Don't apply when modifier key is pressed

* Remove refresh Button from Parameters

* Update apply button styling

* Update apply right distance

* Remove debounce for testing

* Use data-dirty instead of classNames for styling

* Make sure $apply runs before calling onChange
2019-05-15 08:57:06 +03:00
Gabriel Dutra
4f402379e8 Migrate Embed Query Dialog to React (#3783)
* Update Antd

* Migrate Embed Query Dialog to React

* Update jest ScheduleDialog snapshot

* Add Alert for unsafe queries

* Add CodeBlock

* Add inputs to change iframe size

* Undo ant update

* Update share embed spec

* Update styling

* Change border-radius to 2px

* Update margin between Public URL and IFrame Embed
2019-05-15 08:38:40 +03:00
Ran Byron
733b60102d Fixed visual-card alignment (#3795) 2019-05-14 14:10:35 -06:00
Levko Kravets
b9b30a39d2 [Bug fix] Edit parameter mapping: error when trying to change mapping type to Static; cannot change static value (#3800)
* Edit parameter mapping: error when trying to change mapping type to Static

* Parameter mapping editor: cannot change static value
2019-05-14 19:10:02 +03:00
Levko Kravets
c74d469181 resize-event: take into account transformations and transitions (#3794) 2019-05-13 22:51:05 +03:00
Ran Byron
95f11e6686 Loading indicator till app inits (#3788) 2019-05-13 12:11:22 -06:00
Jakdaw
ad6f7109de Fix support for calling MySQL Stored Procedures and allow queries to be cancelled (#3003)
* If MySQL returns multiple resultSets (eg when executing multiple statements, or calling stored procedures) then use the last resultSet that has columns

* Make cancellation of a MySQL query work (the same way the C client does it)

* Address code climate moans
2019-05-13 18:51:51 +03:00
taminif
b09ae46a9f filtered tag remove empty name at edit query (#3784)
* filtered tag remove empty name at edit query

* use filter
2019-05-13 17:57:45 +03:00
Arik Fraimovich
0cda0369f0 [BigQuery] Fix: in some queries there is no mode field (#3786)
Happened with INSERT/UPDATE queries.
2019-05-13 17:08:18 +03:00
Arik Fraimovich
50f11069ce Presto: ignore blank passwords (#3791)
PyHive expects only None as no password.
2019-05-13 12:36:07 +03:00
Arik Fraimovich
6bf764be07 Update query to bring only name to make sure screenshots are consistent. (#3790) 2019-05-12 20:43:32 +03:00
Ran Byron
3159410694 Restrict markdown image dimensions (#3789) 2019-05-12 20:23:10 +03:00
Gabriel Dutra
76bd2e3c50 Migrate Organization Settings to React (#3728)
* Migrate Organization Settings to React

* Fix failing spec and replace default values from inputs

* Add HelpTrigger and handleChange to SAML options

* Undo changes to ant-variables.less

* Add time format to OrganizationSettings
2019-05-12 14:23:22 +03:00
Omer Lachish
50a6f723b1 Fix Ability to Add Query-based Parameters to Existing Queries (#3716)
* propagate `isDirty` down to `QueryBasedParameterInput`

* go to /api/:id/dropdown while editing a query, since dropdown queries might still not be associated with the parent. see #3711

* show helpful error message if dropdown values cannot be fetched

* use backticks instead of line concatenation

* remove requirement to have direct access to dropdown query in order validate it. parent query association checks are sufficient

* remove isDirty-based implementation and allow dropdown queries through nested ACL even if they aren't associated yet (given that the user has _direct_ access to the dropdown query)

* fix tests to cover all cases for /api/queries/:id/dropdowns/:id

* fix indentation

* require access to the query, not the data source

* use require_access instead of has_access
2019-05-12 12:48:01 +03:00
Omer Lachish
0ee20797c8 Fix embeds without parameters (#3775)
* provide queryId when fetching query results in order to allow query-based api-key authentication to work properly

* cypress test to verify that embeds without parameters are shared succesfully

* rename Percy snapshot
2019-05-12 12:28:31 +03:00
taminif
d7515562a4 Fix: Filter empty tags (#3780) 2019-05-10 09:55:53 -06:00
Osmo Salomaa
feafbbe318 Avoid error with duplicate log lines (#3777)
https://docs.angularjs.org/error/ngRepeat/dupes
2019-05-07 22:07:39 +03:00
deecay
b7b345dacd [Feature] Choropleth customize (added new map: Japanese Prefectures) (#3154) 2019-05-07 12:04:17 +03:00
Arik Fraimovich
0b22aa55a1 DynamoDB: safe implementation of schema loading (#3774)
* Safe implementation of describe_all.

* autopep8.
2019-05-06 20:13:45 +03:00
Arik Fraimovich
3eddea6e88 Show non relative timestamp when printing an embed. (#3773)
(Also used for the Slack snapshots)
2019-05-06 20:12:57 +03:00
yoavbls
c85e097f8a [Bug fix] Fix dashboard filters to collect options too (#3759) 2019-05-06 11:40:21 +03:00
YOSHIDA Katsuhiko
81bc4ef58b [Feature] Add direction option in Pie Chart (#3762) 2019-05-06 11:30:48 +03:00
Omer Lachish
9fec3ca9ea Poll for results in parameterized embeds (#3752)
* add an endpoint for fetching job using a query's api_key

* when unauthenticated, use api_key to get job, and fetch the latest query
result (as opposed to fetching the query result by ID)

* add 'refresh dataset' button to parameters directive

* fix scope error introduced by earlier commit

* show timer when refreshing results

* Show input for missing parameters in embedded visualizations (#3741)

* Redirect to default parameter values when parameters are missing in
embedded visualizations

* Revert "Redirect to default parameter values when parameters are missing in"

This reverts commit 43c65500b7.

* load all data after page is loaded

* return no data only when parameters are missing

* data binding no longer required

* show an error on embeds that fail to load

* data binding no longer required

* present full-page error when dealing with unsafe queries

* don't render the execute button for each parameter

* show 'missing parameter value' error

* Don't reload the whole page when parameter value changes.

* Set API key and load config before rendering.

* Add Query#hasParameters method.

* Don't show download controls for parameterized queries (they won't work).

* Use getUrl to construct a correct query link.

* WIP: have a single way to load results

1. This preloads the query before rendering the page, so we can benefit from using default parameters & make the logic in component simpler.
2. Use a single way to load results, to make sure we do polling when try to load the query results for the first time.

* Show persistent errors and finish loading logic.

* Check if query is safe and show message otherwise.

* Fix test for unsafe parameters embed.

* wait for query results to return before taking snapshot
2019-05-06 09:14:56 +03:00
Arik Fraimovich
ee29cf9efc Fix: pie chart not rendering when series doesn't exist in options. (#3756) 2019-05-05 09:04:52 +03:00
Arik Fraimovich
17aba39636 Fix: default value for Presto password should be None (#3757) 2019-05-05 09:04:42 +03:00
Yusuke Goto
2cd1b07a41 Add: organization setting for time format (#3754)
* Support for time format

* Add selects test

* Rename into date_time_format_config
2019-05-05 09:03:27 +03:00
taminif
72d00314a4 [Code style] Add semi-colons (#3755) 2019-05-02 22:30:05 +03:00
Aidarbek Suleimenov
5b077ab083 Support for Presto password (#3619) 2019-05-01 17:25:59 +03:00
Takuya Arita
da2d6bc3a8 Move is_url_key method to function for testability. (#3750) 2019-05-01 13:52:41 +03:00
Takuya Arita
33930a5b9c Remove unused import statements (#3751) 2019-05-01 13:51:56 +03:00
John Karahalis
fbff4f9219 Convert query control dropdown button to React (#3698) 2019-05-01 07:20:54 +03:00
Jannis Leidel
30f725f1e1 Add missing parameter to new BigQuery query runner method. (#3747) 2019-04-30 21:16:58 +02:00
Gabriel Dutra
47cd05b48e Cypress: Fix Stuck E2E test - create_query_spec (#3748) 2019-04-30 14:21:26 -03:00
Levko Kravets
9a4433bf68 Migrate visualizations registry/renderer/editor to React (#3493) 2019-04-30 16:34:00 +03:00
Ran Byron
d0b2151b4d Fix query page height (#3744) 2019-04-29 23:29:17 +03:00
Omer Lachish
21e22a2d0d add get_by_id to Organization (#3712) 2019-04-29 21:58:29 +03:00
Gabriel Dutra
f3a653c57f Fix query based parameter has value null when created (#3707)
* Fix query based parameter value null when created

* Use toString to avoid having 'null' string
2019-04-29 21:50:04 +03:00
guwenqing
c9bf412240 Update npm run to fix hpe_header_overflow (#3732)
Nodejs has set max header size to 8k in http_parser,
need to provide a larger header size to make the proxy work.
2019-04-29 21:23:06 +03:00
Osmo Salomaa
48955b5fa1 Use monospace font in query output log (#3743)
Closes #3739
2019-04-29 21:21:51 +03:00
Ran Byron
24a5748528 Dashboard grid markings (#3656) 2019-04-29 15:49:09 +03:00
Arik Fraimovich
8758279b14 Use REDASH_BASE_PATH everywhere instead of hardcoded path (#3740)
Closes #3727
2019-04-29 14:28:16 +03:00
Jannis Leidel
99bb24d899 Make creating the BigQuery job data pluggable. (#3742)
This would for example allow adding custom job labels (https://cloud.google.com/bigquery/docs/adding-using-labels#job-label) for easier accounting.
2019-04-29 14:18:36 +03:00
Omer Lachish
c93a905c1d Fix Ability to save with Multiple Dropdown Parameters (#3717)
* support multiple associations of the same query-based dropdown parameter

* include several query-based parameters in association tests
2019-04-28 14:25:26 +03:00
AntonZ
a1e75d2f0b feature: add couchbase query runner (#3658)
* feature: add couchbase query runner

* fix style

* fix style

* fix style

* fix naming due to convention

* extracting protocol as parameter
2019-04-24 20:13:59 +03:00
Ran Byron
fb48bc374a Refactored dashboard drag/resize testing (#3726) 2019-04-22 10:07:22 +03:00
Ran Byron
10a6ccbbcd Dashboard save fail indication (#3715) 2019-04-19 21:41:35 +03:00
Gabriel Dutra
fea082ec77 Update Percy network idle timeout (#3724) 2019-04-19 10:58:37 -03:00
Jannis Leidel
aa9d2466cd Split redash/__init__.py to prevent import time side-effects. (#3601)
## What type of PR is this? (check all applicable)
<!-- Please leave only what's applicable -->

- [x] Refactor
- [x] Bug Fix

## Description

This basically makes sure that when import the redash package we don't accidentally trigger import-time side-effects such as requiring Redis.

Refs #3569 and #3466.
2019-04-18 18:39:38 +02:00
Arik Fraimovich
97492d7aa0 Fix: update default CSP policy to allow KB iframe. (#3714)
## What type of PR is this? (check all applicable)

- [x] Bug Fix

## Description

Without this change the Help Drawer couldn't load content anymore.

## Related Tickets & Documents

#3404
2019-04-17 10:13:45 +02:00
Ran Byron
18761cf07b Dashboard auto-saving (#3653) 2019-04-17 10:07:48 +03:00
Arik Fraimovich
9b3dd82ec0 Sync PyAthena/botocore versions with requirements_all_ds.txt. (#3713) 2019-04-17 09:43:33 +03:00
Arik Fraimovich
e485c964c5 Add rate limits to user creation/update (#3709)
* Add rate limits for user resources.

* Disable rate limiting in tests (except for tests that need it).

* Update strings to unicode to avoid SQLA warnings
2019-04-15 13:58:30 +03:00
Omer Lachish
5b30d081d7 Dynamic query time limits (#3702)
* extract time limit decisions to a dynamic settings function

* introduce environment variable for scheduled query time limits

* pass in org_id to query_time_limit

* add an interaction test that verifies that time limits are applied to
jobs

* really important newlines according to CodeClimate
2019-04-15 12:06:37 +03:00
Omer Lachish
b96094b878 add a test to make sure reset password form are displayed correctly (#3678) 2019-04-14 14:59:21 +03:00
Gabriel Dutra
1f43537304 Update CardsList to use visual-card styling (#3679)
* Update CardsList to use old markup

* CR
2019-04-14 13:10:40 +03:00
Aidarbek Suleimenov
01e64db0dc Fix Decimal128 error (#3684) 2019-04-14 13:07:12 +03:00
Arik Fraimovich
3ab46bb39a BigQuery: support for NaN values. (#3701) 2019-04-14 11:23:14 +03:00
Jakdaw
af168c69b9 Fix search ordered by best match (#3706)
* Don't force an order by created date - any usecases that want
this already request it explicitly and it breaks the search
function that wants to order by best match.

* Fix the logic so that we fall back to a default search order when there's
no search term, rather than when there is one and we should use the
best-match ordering.

* Remove accidentially added blank line
2019-04-14 10:44:28 +03:00
Jakdaw
63e052c3a3 Support LDAP servers where one doesn't first have to bind to the LDAP server with a username/password (#3002)
* Support LDAP servers where one doesn't first have to bind to the LDAP server with a username/password

* Address code climate things
2019-04-14 10:36:41 +03:00
Ran Byron
563e34a816 Fixed public dashboard footer (#3703) 2019-04-14 10:19:12 +03:00
Gabriel Dutra
1524d06149 Percy: Introduce hide-in-percy and hide diff problematic elements (#3689) 2019-04-11 11:49:39 -03:00
Ran Byron
e9711a0b9c Bye footer (#3697) 2019-04-10 11:42:36 +03:00
Omer Lachish
9fcf510ffd add package.json after including qs (#3695) 2019-04-10 11:25:56 +03:00
Ran Byron
70227f2e43 Changed viz embed download menu to drop up (#3696) 2019-04-10 11:02:03 +03:00
Ran Byron
1babd01f38 Bolder markdown in textbox (#3686) 2019-04-09 10:25:03 +03:00
Ran Byron
768bfb3525 Cypress Dashboard Service (#3683) 2019-04-09 08:49:10 +03:00
Ran Byron
fc5a624efb Dashboard one column mode test (#3621) 2019-04-08 07:51:18 +03:00
Omer Lachish
47bf91e150 Fix: support date ranges for parameterized embeds (#3681)
* support date ranges for parameterized embeds

* add qs

* remove hideous implementation and use qs

* get rid of apiKey querystring parameter and introduce query-string
module
2019-04-07 20:34:14 +03:00
Ran Byron
8f4288583e Updated Cypress default timeout values (#3685) 2019-04-07 16:40:06 +03:00
Omer Lachish
595af3bce8 avoid erroring when creating embed links for queries that don't have any parameters (#3680) 2019-04-06 13:49:40 +03:00
HirokiTanaka
dba7efe030 refs https://github.com/getredash/redash/issues/3675 (#3676) 2019-04-04 09:27:07 +03:00
Omer Lachish
1b142b33f1 reduce volatility in embed percy snapshots (#3672) 2019-04-02 13:40:28 +03:00
Gabriel Dutra
13814c752d Add max-width to Notification (#3667) 2019-04-02 11:53:44 +03:00
Omer Lachish
dd477d49ec Sharing embeds with safe parameters (#3495)
* change has_access and require_access signatures to work with the objects that require access, instead of their groups

* change has_access and require_access signatures to work with the objects that require access, instead of their groups

* use the textless endpoint (/api/queries/:id/results) for pristine
queriest

* Revert "use the textless endpoint (/api/queries/:id/results) for pristine"

This reverts commit cd2cee7738.

* go to textless /api/queries/:id/results by default

* change `run_query`'s signature to accept a ParameterizedQuery instead of
constructing it inside

* raise HTTP 400 when receiving invalid parameter values. Fixes #3394

* support querystring params

* extract coercing of numbers to function, along with a friendlier
implementation

* wire embeds to textless endpoint

* allow users with view_only permissions to execute queries on the
textless endpoint, as it only allows safe queries to run

* enqueue jobs for ApiUsers

* add parameters component for embeds

* include existing parameters in embed code

* fetch correct values for json requests

* remove previous embed parameter code

* rename `id` to `user_id`

* support executing queries using Query api_keys by instantiating an ApiUser that would be able to execute the specific query

* bring back ALLOW_PARAMETERS_IN_EMBEDS (with link on deprecation coming up)

* show deprecation messages for ALLOW_PARAMETERS_IN_EMBEDS. Also, move
other message (email not verified) to use the same mechanism

* add link to forum message on setting deprecation

* rephrase deprecation message

* add link to forum message regarding embed deprecation

* change API to /api/queries/:id/dropdowns/:dropdown_id

* split to 2 different dropdown endpoints and implement the second

* add test cases for /api/queries/:id/dropdowns/:id

* use new /dropdowns endpoint in frontend

* first e2e test for sharing embeds

* Pleasing the CodeClimate overlords

* All glory to CodeClimate

* change has_access and require_access signatures to work with the objects that require access, instead of their groups

* split has_access between normal users and ApiKey users

* remove residues from bad rebase

* allow access to safe queries via api keys

* rename `object` to `obj`

* support both objects and group dicts in `has_access` and `require_access`

* simplify permission tests once `has_access` accepts groups

* change has_access and require_access signatures to work with the objects that require access, instead of their groups

* rename `object` to `obj`

* support both objects and group dicts in `has_access` and `require_access`

* simplify permission tests once `has_access` accepts groups

* fix bad rebase

* send embed parameters through POST data

* no need to log `is_api_key`

* move query fetching by api_key to within the Query model

* fetch user by adding a get_by_id function on the User model

* pass parameters as POST data (fixes test failure introduced by switching
from query string parameters to POST data)

* test the right thing - queries with safe parameters should be embeddable

* introduce cy.clickThrough

* add another Cypress test to make sure unsafe queries cannot be embedded

* serialize Parameters into query string

* set is_api_key as the last parameter to (hopefully) avoid
backward-dependency problems

* Update redash/models/parameterized_query.py

Co-Authored-By: rauchy <omer@rauchy.net>

* attempt to fix empty percy snapshots

* snap percies after DOM is fully loaded
2019-04-02 11:45:38 +03:00
Ran Byron
5decd2624a Fixed wrong width assertion (#3665) 2019-04-01 13:49:24 +03:00
Justin Clift
6f9aee42a7 Update to modern Redis for the docker images (#3640) 2019-04-01 11:21:06 +03:00
Omer Lachish
1333aae7fb Handle dropdown queries which are detached from data source (#3453)
* handle an edge case where dropdown queries are connected to data sources
that no longer exist

* Rethinking it, an empty result set makes no sense and it's better to
throw an error

* remove redundant import
2019-04-01 11:19:52 +03:00
Omer Lachish
33ad89a381 in case of a parameter type mismatch, show the actual message to the user (#3664) 2019-04-01 11:19:18 +03:00
Ran Byron
02a5852072 Widget size and position test (#3628) 2019-03-29 21:47:26 +03:00
Gabriel Dutra
12782e4daf Fix Percy diff due to Api Key secret (#3654) 2019-03-29 07:50:30 -03:00
Levko Kravets
704b78a003 [Feature, Bug fix] Migrate Timer component to React; update TimeAgo component (#3648) 2019-03-28 20:33:05 +02:00
Omer Lachish
ec4f77c8b7 Change has_access and require_access signatures (#3611)
* change has_access and require_access signatures to work with the objects that require access, instead of their groups

* rename `object` to `obj`

* support both objects and group dicts in `has_access` and `require_access`

* simplify permission tests once `has_access` accepts groups
2019-03-28 15:01:06 +02:00
Ran Byron
1871287a1f Fixed notification alignment (#3645) 2019-03-28 10:08:13 +02:00
Gabriel Dutra
f9cc230227 Migrate Data Sources and Alert Destinations pages to React (#3470)
* Migrate TypePicker to React

* Migrate DataSources and Destinations List

* Fixes to DestinationsList

* Add CreateDataSource (testing with Steps)

* Render the form after type selection

* Add HELP_LINKS to CreateDataSource

* Add Done behavior

* Add scrollToTop to CreateDataSource

* TypePicker styling adjusts

* Add CreateDestination

* Update resouce gets to componentDidMount

* Create EditForm components

* Migrate Edit pages

* Remove angular logic from DynamicForm

* Add actions to EditPages

* TypePicker title style adjustments

* Add Empty and Loading state

* UX improvements

* Review changes

* Styling updates on TypePicker, forms background fix

* Add blank line removed by mistaken

* Reorganize TypePicker

* Hide Search on List Pages

* Fix spacing in Forms

* Update Create Data Source and Destination to be a Dialog

* Remove max-height from the form

* Fix DynamicForm import in CreateUserDialog

* Route /new to open CreateSourceDialog

* Add HelpTrigger + refine styling and Edit Pages

* Remove help links from data source resource

* Update Cypress specs

* TypePicker -> CardsList

* Remove old TypePicker styling and change CardsList styling to less

* Test if Percy shows Dialogs

* Personal review cleanup

* CR

* Remove unnecessary query on dialog success

* Handle resource errors in Edit Pages

* Add CreateDestination policy

* Add placeholder and separator to the Name field

* Use cy.click instead of cy.wait

* Revert "Use cy.click instead of cy.wait" (Didn't work)

This reverts commit 77285d9fa3.

* Align help trigger on the right and rename Steps

* Refine behavior for long names

* Update toastr calls to use notification instead

* Redirect to target after creation

* Remove autoFocus on DynamicForm for Edit Pages

* Add eslint-disable for cy.wait
2019-03-28 10:06:46 +02:00
Ran Byron
fe4a7b65e7 Widget resize tests (#3620) 2019-03-28 05:55:03 +02:00
Allen Short
b3819de878 Treat repeated BigQuery fields as arrays (#3480)
* Treat repeated BigQuery fields as arrays

* handle untransformed field types and None
2019-03-27 22:00:09 +02:00
Gabriel Dutra
2699d24441 Manage user groups in UserEdit (#3450) 2019-03-27 16:29:48 -03:00
Jannis Leidel
1933dee8ca Fix Celery worker --max-tasks-per-child for Celery 4.x. (#3625)
* Fix Celery worker CLI parameter name that was changed in Celery 4.x.

* Set Celery worker --max-memory-per-child to 1/4th of total system memory.

* Review fixes.

* Review fixes.
2019-03-27 21:08:20 +02:00
Gabriel Dutra
375e61f263 Add error message when destination name already exists (#3597)
* Return 400 when destination name already exists

* Remove whitespace

* Unicode 1

Co-Authored-By: gabrieldutra <nesk.frz@gmail.com>

* Unicode 2

Co-Authored-By: gabrieldutra <nesk.frz@gmail.com>
2019-03-27 18:09:56 +02:00
shinsuke-nara
872d0ca5e6 Show accessible tables only in New Query view for PostgreSQL (#3599)
* Show accessible tables only.

* Get table information from information_schema.columns.

* Union old query.
2019-03-27 18:08:38 +02:00
Justin Clift
973ad565cd Update PostgreSQL version to always use latest in the 9.5 series (#3639) 2019-03-27 18:06:40 +02:00
ialeinikov
7a7fdf9c99 allowing to specify a custom work group for AWS Athena queries (#3592)
* allowing to specify a custom work group for AWS Athena queries

* Fixing title + adding correct position in the UI
2019-03-27 17:58:48 +02:00
Omer Lachish
49ffaae3ec Fix email shows as unverified when no email server is configured (#3613)
* check that e-mail server is configured before marking the email address
as not verified and sending out a verification e-mail

* use helper method in `invite_user`

* move email_server_configured helper to settings

* add test to verify that email addresses arent marked as unverified if
there's no e-mail server to verify them

* simplify a couple of tests with patch

* combine conditions into single variable

* Booleans, gotta love 'em
2019-03-27 17:57:51 +02:00
Allen Short
d5494cff08 Fail query task properly even if error message is empty (#3499) 2019-03-27 17:50:39 +02:00
Byunghwa Yun
71afc99ec3 Add phoenix query runner. (#3153)
* Add phoenix query runner.

* Improved error handling.
2019-03-27 17:48:49 +02:00
Ran Byron
b5d97e25b7 Browser support config (#3609)
* Browser support config

* Removed some offending code

* Added unsupported html page and redirect for IE

* Typo in regex

* Made html page static

* Added redirect script to multi_org

* Moved static html page to client/app
2019-03-27 17:47:12 +02:00
Jannis Leidel
6c26aa7a99 Render LDAP and remote auth login links correctly when multi org mode is enabled. (#3530)
* Make LDAP auth handler org scoped.

* Render LDAP and remote auth login links correctly when multi org mode is enabled.
2019-03-27 17:26:00 +02:00
Jannis Leidel
712fc63f93 Use flask-talisman for handling backend response headers (#3404)
* Normalize Flask initialization API use.

* Use Flask-Talisman.

* Enable HSTS when HTTPS is enforced.

* More details about how CSP is formatted and write CSP directives as a string.

* Use CSP frame-ancestors directive and not X-Frame-Options for embedable endpoints.

* Add link to flask-talisman docs.

* set remember_token cookie to be HTTP-Only and Secure

* Reorganize secret key configuration to be forward thinking and backward compatible.
2019-03-27 17:24:15 +02:00
Jannis Leidel
77c53130a4 Fix a few more inconsistencies when loading and dumping JSON. (#3626)
* Fix a few more inconsistencies when loading and dumping JSON.

Refs #2807. Original work in: #2817

These change have been added since c2429e92d2.

* Review fixes.
2019-03-27 17:14:32 +02:00
Levko Kravets
73c8e3096d [Feature] Migrate Admin pages to React (#3568) 2019-03-27 09:48:50 +02:00
Ran Byron
8230098f50 Migrated Textbox edit dialog to React (#3632) 2019-03-26 19:23:00 +02:00
Arik Fraimovich
fd42091f87 Add Lint step to CircleCI (#3642) 2019-03-26 16:40:26 +02:00
Ran Byron
ec4b36b178 Cypress eslint fixes and config (#3636) 2019-03-25 22:14:51 +02:00
Ran Byron
0995dfbf43 Widget drag tests (#3598) 2019-03-25 19:16:41 +02:00
Gabriel Dutra
70d4c724c2 Add env var to skip Flask rate limits (#3622) 2019-03-25 13:15:20 -03:00
Justin Clift
1d7378f84b Update docker compose with the stable Redash v7 tag (#3638) 2019-03-25 14:50:40 +02:00
Gabriel Dutra
b4a4ee212e Replace toastr with Ant Notification (#3610) 2019-03-24 19:08:35 -03:00
Gabriel Dutra
25910e7655 Move cypress to client folder (#3566) 2019-03-24 11:24:59 -03:00
Jannis Leidel
8e5ba804f6 Fix a DeprecationWarning about the Flask.static_path parameter. (#3624)
Code: d1d82ca8ce/flask/app.py (L347-L351)
2019-03-24 15:57:35 +02:00
Arik Fraimovich
173f9ba7e8 Fix: triggers not created for queries.search_vector (#3631) 2019-03-24 15:35:59 +02:00
Arik Fraimovich
e712c19bbe E2E test for query search (#3633)
* Apply prettier to app-header.html.
* Add: E2E test for query search
2019-03-24 15:20:08 +02:00
Ran Byron
aea3c9dbaa Fix for time based mongodb test (#3630) 2019-03-24 11:29:44 +02:00
Ran Byron
2f8aade697 Added a skipped test for issue #3202 (#3616) 2019-03-23 22:02:39 +02:00
Ran Byron
a7b930a422 Widget tests - add, remove, auto height (#3590) 2019-03-23 14:27:43 +02:00
Levko Kravets
4e69b73b0f [Bug fix] Query Results fails to use query which has double quotes in column names (#3618) 2019-03-21 19:39:22 +02:00
Omer Lachish
c47dd05095 Nest query ACL to dropdowns (#3544)
* change API to /api/queries/:id/dropdowns/:dropdown_id

* extract  property

* split to 2 different dropdown endpoints and implement the second

* make access control optional for dropdowns (assuming it is verified at a
different level)

* add test cases for /api/queries/:id/dropdowns/:id

* use new /dropdowns endpoint in frontend

* require access to dropdown queries when creating or updating parent
queries

* rename Query resource dropdown endpoints

* check access to dropdown query associations in one fugly query

* move ParameterizedQuery to models folder

* add dropdown association tests to query creation

* move group by query ids query into models.Query

* use bound parameters for groups query

* format groups query

* use new associatedDropdowns endpoint in dashboards

* pass down parameter and let it return dropdown options. Go Levko!

* change API to /api/queries/:id/dropdowns/:dropdown_id

* split to 2 different dropdown endpoints and implement the second

* use new /dropdowns endpoint in frontend

* pass down parameter and let it return dropdown options. Go Levko!

* fix bad rebase

* add comment to clarify the purpose of checking the queryId
2019-03-20 09:16:10 +02:00
Arik Fraimovich
15c815fb5e Remove node_modules before creating tarball (#3603)
* Update pack

* Remove node_modules before packing
2019-03-18 12:16:31 +02:00
Arik Fraimovich
9de676acee Fix: make sure that only the top level node_modules directory is excluded (#3600)
* Fix: make sure that only the top level node_modules directory is excluded

* Remove old unused packing script
2019-03-18 11:13:42 +02:00
596 changed files with 23416 additions and 10674 deletions

View File

@@ -6,7 +6,7 @@ WORKDIR $APP
COPY package.json $APP/package.json COPY package.json $APP/package.json
RUN npm run cypress:install > /dev/null RUN npm run cypress:install > /dev/null
COPY cypress $APP/cypress COPY client/cypress $APP/client/cypress
COPY cypress.json $APP/cypress.json COPY cypress.json $APP/cypress.json
RUN ./node_modules/.bin/cypress verify RUN ./node_modules/.bin/cypress verify

View File

@@ -39,26 +39,40 @@ jobs:
name: Copy Test Results name: Copy Test Results
command: | command: |
mkdir -p /tmp/test-results/unit-tests mkdir -p /tmp/test-results/unit-tests
docker cp tests:/app/coverage.xml ./coverage.xml docker cp tests:/app/coverage.xml ./coverage.xml
docker cp tests:/app/junit.xml /tmp/test-results/unit-tests/results.xml docker cp tests:/app/junit.xml /tmp/test-results/unit-tests/results.xml
- store_test_results: - store_test_results:
path: /tmp/test-results path: /tmp/test-results
- store_artifacts: - store_artifacts:
path: coverage.xml path: coverage.xml
frontend-lint:
docker:
- image: circleci/node:8
steps:
- checkout
- run: mkdir -p /tmp/test-results/eslint
- run: npm install
- run: npm run lint:ci
- store_test_results:
path: /tmp/test-results
frontend-unit-tests: frontend-unit-tests:
docker: docker:
- image: circleci/node:8 - image: circleci/node:8
steps: steps:
- checkout - checkout
- run: sudo apt install python-pip - run: sudo apt install python-pip
- run: sudo pip install -r requirements_bundles.txt
- run: npm install - run: npm install
- run: npm run bundle - run: npm run bundle
- run: npm test - run: npm test
- run: npm run lint
frontend-e2e-tests: frontend-e2e-tests:
environment: environment:
COMPOSE_FILE: .circleci/docker-compose.cypress.yml COMPOSE_FILE: .circleci/docker-compose.cypress.yml
COMPOSE_PROJECT_NAME: cypress COMPOSE_PROJECT_NAME: cypress
PERCY_TOKEN_ENCODED: ZGRiY2ZmZDQ0OTdjMzM5ZWE0ZGQzNTZiOWNkMDRjOTk4Zjg0ZjMxMWRmMDZiM2RjOTYxNDZhOGExMjI4ZDE3MA== PERCY_TOKEN_ENCODED: ZGRiY2ZmZDQ0OTdjMzM5ZWE0ZGQzNTZiOWNkMDRjOTk4Zjg0ZjMxMWRmMDZiM2RjOTYxNDZhOGExMjI4ZDE3MA==
CYPRESS_PROJECT_ID_ENCODED: OTI0Y2th
CYPRESS_RECORD_KEY_ENCODED: YzA1OTIxMTUtYTA1Yy00NzQ2LWEyMDMtZmZjMDgwZGI2ODgx
docker: docker:
- image: circleci/node:8 - image: circleci/node:8
steps: steps:
@@ -72,30 +86,20 @@ jobs:
name: Setup Redash server name: Setup Redash server
command: | command: |
npm run cypress start npm run cypress start
docker-compose run cypress node ./cypress/cypress.js db-seed docker-compose run cypress npm run cypress db-seed
- run: - run:
name: Execute Cypress tests name: Execute Cypress tests
command: npm run cypress run-ci command: npm run cypress run-ci
build-tarball: build-docker-image:
docker: docker:
- image: circleci/node:8 - image: circleci/node:8
steps:
- checkout
- run: sudo apt install python-pip
- run: npm install
- run: .circleci/update_version
- run: npm run bundle
- run: npm run build
- run: .circleci/pack
- store_artifacts:
path: /tmp/artifacts/
build-docker-image:
docker:
- image: circleci/buildpack-deps:xenial
steps: steps:
- setup_remote_docker - setup_remote_docker
- checkout - checkout
- run: sudo apt install python-pip
- run: sudo pip install -r requirements_bundles.txt
- run: .circleci/update_version - run: .circleci/update_version
- run: npm run bundle
- run: .circleci/docker_build - run: .circleci/docker_build
workflows: workflows:
version: 2 version: 2
@@ -104,19 +108,15 @@ workflows:
- python-flake8-tests - python-flake8-tests
- legacy-python-flake8-tests - legacy-python-flake8-tests
- backend-unit-tests - backend-unit-tests
- frontend-unit-tests - frontend-lint
- frontend-e2e-tests - frontend-unit-tests:
- build-tarball:
requires: requires:
- backend-unit-tests - frontend-lint
- frontend-unit-tests - frontend-e2e-tests:
- frontend-e2e-tests requires:
filters: - frontend-lint
branches: - hold:
only: type: approval
- master
- /release\/.*/
- build-docker-image:
requires: requires:
- backend-unit-tests - backend-unit-tests
- frontend-unit-tests - frontend-unit-tests
@@ -127,3 +127,6 @@ workflows:
- master - master
- preview-image - preview-image
- /release\/.*/ - /release\/.*/
- build-docker-image:
requires:
- hold

View File

@@ -13,6 +13,7 @@ services:
REDASH_LOG_LEVEL: "INFO" REDASH_LOG_LEVEL: "INFO"
REDASH_REDIS_URL: "redis://redis:6379/0" REDASH_REDIS_URL: "redis://redis:6379/0"
REDASH_DATABASE_URL: "postgresql://postgres@postgres/postgres" REDASH_DATABASE_URL: "postgresql://postgres@postgres/postgres"
REDASH_RATELIMIT_ENABLED: "false"
worker: worker:
build: ../ build: ../
command: scheduler command: scheduler
@@ -38,6 +39,12 @@ services:
PERCY_BRANCH: ${CIRCLE_BRANCH} PERCY_BRANCH: ${CIRCLE_BRANCH}
PERCY_COMMIT: ${CIRCLE_SHA1} PERCY_COMMIT: ${CIRCLE_SHA1}
PERCY_PULL_REQUEST: ${CIRCLE_PR_NUMBER} PERCY_PULL_REQUEST: ${CIRCLE_PR_NUMBER}
COMMIT_INFO_BRANCH: ${CIRCLE_BRANCH}
COMMIT_INFO_AUTHOR: ${CIRCLE_USERNAME}
COMMIT_INFO_SHA: ${CIRCLE_SHA1}
COMMIT_INFO_REMOTE: ${CIRCLE_REPOSITORY_URL}
CYPRESS_PROJECT_ID: ${CYPRESS_PROJECT_ID}
CYPRESS_RECORD_KEY: ${CYPRESS_RECORD_KEY}
redis: redis:
image: redis:3.0-alpine image: redis:3.0-alpine
restart: unless-stopped restart: unless-stopped

View File

@@ -6,4 +6,4 @@ FILENAME=$NAME.$FULL_VERSION.tar.gz
mkdir -p /tmp/artifacts/ mkdir -p /tmp/artifacts/
tar -zcv -f /tmp/artifacts/$FILENAME --exclude="optipng*" --exclude=".git*" --exclude="*.pyc" --exclude="*.pyo" --exclude="venv" --exclude="node_modules" * tar -zcv -f /tmp/artifacts/$FILENAME --exclude=".git" --exclude="optipng*" --exclude="cypress" --exclude="*.pyc" --exclude="*.pyo" --exclude="venv" *

View File

@@ -21,20 +21,12 @@ plugins:
pep8: pep8:
enabled: true enabled: true
eslint: eslint:
enabled: true enabled: false
channel: "eslint-5"
config:
config: client/.eslintrc.js
checks:
import/no-unresolved:
enabled: false
no-multiple-empty-lines: # TODO: Enable
enabled: false
exclude_patterns: exclude_patterns:
- "tests/**/*.py" - "tests/**/*.py"
- "migrations/**/*.py" - "migrations/**/*.py"
- "setup/**/*" - "setup/**/*"
- "bin/**/*" - "bin/**/*"
- "**/node_modules/" - "**/node_modules/"
- "client/dist/" - "client/dist/"
- "**/*.pyc" - "**/*.pyc"

View File

@@ -9,6 +9,6 @@ trim_trailing_whitespace = true
indent_style = space indent_style = space
indent_size = 4 indent_size = 4
[*.{js,css,html}] [*.{js,jsx,css,less,html}]
indent_style = space indent_style = space
indent_size = 2 indent_size = 2

4
.gitignore vendored
View File

@@ -24,5 +24,5 @@ node_modules
.sass-cache .sass-cache
npm-debug.log npm-debug.log
cypress/screenshots client/cypress/screenshots
cypress/videos client/cypress/videos

View File

@@ -1,5 +1,118 @@
# Change Log # Change Log
## v8.0.0-beta.2 - 2019-09-16
This is an update to the previous beta release, which includes:
* Add options for users to share anonymous usage information with us (see [docs](https://redash.io/help/open-source/admin-guide/usage-data) for details).
* Visualizations:
- Allow the user to decide how to handle null values in charts.
* Upgrade Sentry-SDK to latest version.
* Make horizontal table scroll visible in dashboard widgets without scrolling.
* Data Sources:
* Add support for Azure Data Explorer (Kusto).
* MySQL: fix connections without SSL configuration failing.
* Amazon Redshift: option to set query group for adhoc/scheduled queries.
* Hive: make error message more friendly.
* Qubole: add support to run Quantum queries.
* Display data source icon in query editor.
* Fix: allow users with view only acces to use the queries in Query Results
* Dashboard: when updating parameters refersh only widgets that use those parameters.
This release had contributions from 12 people: @arikfr, @cclauss, @gabrieldutra, @justinclift, @kravets-levko, @ranbena, @rauchy, @sandeepV2, @shinsuke-nara, @spacentropy, @sphenlee, @swfz.
## v8.0.0-beta - 2019-08-18
After months of being heads down with hard work, it's finally time to wrap up the V8 release 🤩 This release includes many long awaited improvements to parameters, UX improvements, further React migration and other changes, fixes and improvements.
While this version is already running on the hosted platform to make sure it's stable, we're excited to put this in the hands of our Open Source users.
Starting from this release we will no longer build a tarball distribution of the codebase and recommend everyone to switch over to using our Docker images. We're planning on dropping Python 2 support towards its EOL this year and switching over to the Docker image will make this transition much simpler.
This release was made possible by contributions from over 40 people: @aidarbek, @AntonZarutsky, @ariarijp, @arikfr, @combineads, @deecay, @fmy, @gabrieldutra, @guwenqing, @guyco33, @ialeinikov, @Jakdaw, @jezdez, @justinclift, @k-tomoyasu, @katty0324, @koooge, @kravets-levko, @ktmud, @KumanoTanaka, @kyoshidajp, @nason, @oldPadavan, @openjck, @osule, @otsaloma, @ranbena, @rauchy, @rueian, @sekiyama58, @shinsuke-nara, @taminif, @The-Alchemist, @vv-p, @washort, @wudi-ayuan, @ygrishaev, @yoavbls, @yoshiken, @yusukegoto and the support of over 500 organizations who subscribed to our hosted version and by that sponsor the team's work.
### Parameters
- Parameter UI improvements:
- Support for multi-select in dropdown (and query dropdown) parameters.
- Support for dynamic values in date and date-range parameters.
- Search dropdown parameter values.
- New UX for applying parameter changes in queries and dashboards.
- Allow using Safe Parameters in visualization embeds and public dashboards. Safe Parameters are any parameter type except for the a text parameter (dropdowns are safe).
### Data Sources
- New Data Sources: Couchbase, Phoenix and Dgraph.
- New JSON data source (and deprecated old URL data source).
- Snowflake: update connector to latest version.
- PostgreSQL: show only accessible tables in schema.
- BigQuery:
- Correctly handle NaN values.
- Treat repeated fields as rrays.
- [BigQuery] Fix: in some queries there is no mode field
- DynamoDB:
- Support for Unicode in queries.
- Safe loading of schema.
- Rockset: better handling of query errors.
- Google Sheets:
- Support for Team Drive.
- Friendlier error message in case of an API error and more reliable test connection.
- MySQL:
- Support for calling Stored Procedures and better handling of query cancellation.
- Switch to using `mysqlclient` (a maintained fork of `Python-MySQL`).
- MongoDB: Support serializing Decimal128 values.
- Presto: support for passwords in connection settings.
- Amazon Athena: allow to specify custom work group.
- Query Results: querying a column with a dictionary or array fails
- Clickhouse: make sure we don't show password in error messages.
- Enable Cassandra support by default.
### Visualizations
- Charts:
- Fix: legend overlapping chart on small screens.
- Fix: Pie chart not rendering when series doesn't exist in options.
- Pie Chart: add option to set direction of slices.
- WordCloud: rewritten to support new options (provide frequency in query, limits), scale when resizing, handle long words and more.
- Pivot Table: support hiding totals.
- Counters: apply formatting to target value.
- Maps:
- Ability to customize marker icon and color.
- Customization options for Choropleth maps.
- New Visualization: Details View.
### **UX**
- Replace blank screen with a loading indicator when the application is doing its first load.
- Multiple improvements to dashboards editing: auto-save, grid markings and better refresh indicator.
- Admin can now edit user's groups from the user page.
- Add keyboard shortcut (Ctrl/Cmd+Shift+F) to trigger query formatting.
### API
- Query Result API response minimized to only required fields when called with a non user API key.
- Prefer API key over cookies in authentication.
- User can now regenerate Query API Key.
### Other Changes
- Sends CSP headers to prevent various kinds of security attacks via the browser. Might break unusual usages and embeds of Redash.
- New Failed Scheduled Queries email report (can be enabled from organization settings screen).
- Deprecated HipChat Alert Destination.
- Add options to hide different parts of a Visualization embed UI (parameters, title, link to query).
- Support multi-byte search for query names and descriptions (needs to be enabled in Organization settings screen).
- CSV query results download: correctly serialize booleans and date values.
- Dashboard filters now collect values from all widgets with the same filter.
- Support for custom message and description in alert notifications (currently disabled behind a feature flag until we improve the alert UX).
### Bug Fixes
- Fix: adding widget to dashboard from a query page is broken.
- Fix: default time format option was wrong.
- Fix: when too many errors of a scheduled queries occur it causes an OverflowError.
- Fix: when forking a query maintain the same visualizations order.
## v7.0.0 - 2019-03-17 ## v7.0.0 - 2019-03-17
We're trying a new format for the CHANGELOG in this release. Focusing on the bigger changes, but for whoever interested, you can see all the changes [here](https://github.com/getredash/redash/compare/v6.0.0...master). We're trying a new format for the CHANGELOG in this release. Focusing on the bigger changes, but for whoever interested, you can see all the changes [here](https://github.com/getredash/redash/compare/v6.0.0...master).

View File

@@ -4,17 +4,18 @@ WORKDIR /frontend
COPY package.json package-lock.json /frontend/ COPY package.json package-lock.json /frontend/
RUN npm install RUN npm install
COPY . /frontend COPY client /frontend/client
COPY webpack.config.js /frontend/
RUN npm run build RUN npm run build
FROM redash/base:latest FROM redash/base:debian
# Controls whether to install extra dependencies needed for all data sources. # Controls whether to install extra dependencies needed for all data sources.
ARG skip_ds_deps ARG skip_ds_deps
# We first copy only the requirements file, to avoid rebuilding on every file # We first copy only the requirements file, to avoid rebuilding on every file
# change. # change.
COPY requirements.txt requirements_dev.txt requirements_all_ds.txt ./ COPY requirements.txt requirements_bundles.txt requirements_dev.txt requirements_all_ds.txt ./
RUN pip install -r requirements.txt -r requirements_dev.txt RUN pip install -r requirements.txt -r requirements_dev.txt
RUN if [ "x$skip_ds_deps" = "x" ] ; then pip install -r requirements_all_ds.txt ; else echo "Skipping pip install -r requirements_all_ds.txt" ; fi RUN if [ "x$skip_ds_deps" = "x" ] ; then pip install -r requirements_all_ds.txt ; else echo "Skipping pip install -r requirements_all_ds.txt" ; fi

View File

@@ -1,11 +1,10 @@
<p align="center"> <p align="center">
<img title="Redash" src='https://redash.io/assets/images/logo.png' width="200px"/> <img title="Redash" src='https://redash.io/assets/images/logo.png' width="200px"/>
</p> </p>
<p align="center">
<img title="Build Status" src='https://circleci.com/gh/getredash/redash.png?circle-token=8a695aa5ec2cbfa89b48c275aea298318016f040'/>
</p>
[![Documentation](https://img.shields.io/badge/docs-redash.io/help-brightgreen.svg)](https://redash.io/help/) [![Documentation](https://img.shields.io/badge/docs-redash.io/help-brightgreen.svg)](https://redash.io/help/)
[![Datree](https://s3.amazonaws.com/catalog.static.datree.io/datree-badge-20px.svg)](https://datree.io/?src=badge)
[![Build Status](https://circleci.com/gh/getredash/redash.png?style=shield&circle-token=8a695aa5ec2cbfa89b48c275aea298318016f040)](https://circleci.com/gh/getredash/redash/tree/master)
**_Redash_** is our take on freeing the data within our company in a way that will better fit our culture and usage patterns. **_Redash_** is our take on freeing the data within our company in a way that will better fit our culture and usage patterns.

5
SECURITY.md Normal file
View File

@@ -0,0 +1,5 @@
# Security Policy
## Reporting a Vulnerability
Please email security@redash.io to report any security vulnerabilities. We will acknowledge receipt of your vulnerability and strive to send you regular updates about our progress. If you're curious about the status of your disclosure please feel free to email us again. If you want to encrypt your disclosure email, you can use [this PGP key](https://keybase.io/arikfr/key.asc).

View File

@@ -1,39 +1,118 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Copy bundle extension files to the client/app/extension directory"""
import logging
import os import os
from subprocess import call from pathlib2 import Path
from distutils.dir_util import copy_tree from shutil import copy
from collections import OrderedDict as odict
from pkg_resources import iter_entry_points, resource_filename, resource_isdir from importlib_metadata import entry_points
from importlib_resources import contents, is_resource, path
# Name of the subdirectory
BUNDLE_DIRECTORY = "bundle"
logger = logging.getLogger(__name__)
# Make a directory for extensions and set it as an environment variable # Make a directory for extensions and set it as an environment variable
# to be picked up by webpack. # to be picked up by webpack.
EXTENSIONS_RELATIVE_PATH = os.path.join('client', 'app', 'extensions') extensions_relative_path = Path('client', 'app', 'extensions')
EXTENSIONS_DIRECTORY = os.path.join( extensions_directory = Path(__file__).parent.parent / extensions_relative_path
os.path.dirname(os.path.dirname(__file__)),
EXTENSIONS_RELATIVE_PATH)
if not os.path.exists(EXTENSIONS_DIRECTORY): if not extensions_directory.exists():
os.makedirs(EXTENSIONS_DIRECTORY) extensions_directory.mkdir()
os.environ["EXTENSIONS_DIRECTORY"] = EXTENSIONS_RELATIVE_PATH os.environ["EXTENSIONS_DIRECTORY"] = str(extensions_relative_path)
for entry_point in iter_entry_points('redash.extensions'):
# This is where the frontend code for an extension lives
# inside of its package.
content_folder_relative = os.path.join(
entry_point.name, 'bundle')
(root_module, _) = os.path.splitext(entry_point.module_name)
if not resource_isdir(root_module, content_folder_relative): def resource_isdir(module, resource):
"""Whether a given resource is a directory in the given module
https://importlib-resources.readthedocs.io/en/latest/migration.html#pkg-resources-resource-isdir
"""
try:
return resource in contents(module) and not is_resource(module, resource)
except (ImportError, TypeError):
# module isn't a package, so can't have a subdirectory/-package
return False
def entry_point_module(entry_point):
"""Returns the dotted module path for the given entry point"""
return entry_point.pattern.match(entry_point.value).group("module")
def load_bundles():
""""Load bundles as defined in Redash extensions.
The bundle entry point can be defined as a dotted path to a module
or a callable, but it won't be called but just used as a means
to find the files under its file system path.
The name of the directory it looks for files in is "bundle".
So a Python package with an extension bundle could look like this::
my_extensions/
├── __init__.py
└── wide_footer
├── __init__.py
└── bundle
├── extension.js
└── styles.css
and would then need to register the bundle with an entry point
under the "redash.bundles" group, e.g. in your setup.py::
setup(
# ...
entry_points={
"redash.bundles": [
"wide_footer = my_extensions.wide_footer",
]
# ...
},
# ...
)
"""
bundles = odict()
for entry_point in entry_points().get("redash.bundles", []):
logger.info('Loading Redash bundle "%s".', entry_point.name)
module = entry_point_module(entry_point)
# Try to get a list of bundle files
if not resource_isdir(module, BUNDLE_DIRECTORY):
logger.error(
'Redash bundle directory "%s" could not be found.', entry_point.name
)
continue
with path(module, BUNDLE_DIRECTORY) as bundle_dir:
bundles[entry_point.name] = list(bundle_dir.rglob("*"))
return bundles
bundles = load_bundles().items()
if bundles:
print('Number of extension bundles found: {}'.format(len(bundles)))
else:
print('No extension bundles found.')
for bundle_name, paths in bundles:
# Shortcut in case not paths were found for the bundle
if not paths:
print('No paths found for bundle "{}".'.format(bundle_name))
continue continue
content_folder = resource_filename(root_module, content_folder_relative) # The destination for the bundle files with the entry point name as the subdirectory
destination = Path(extensions_directory, bundle_name)
if not destination.exists():
destination.mkdir()
# This is where we place our extensions folder. # Copy the bundle directory from the module to its destination.
destination = os.path.join( print('Copying "{}" bundle to {}:'.format(bundle_name, destination.resolve()))
EXTENSIONS_DIRECTORY, for src_path in paths:
entry_point.name) dest_path = destination / src_path.name
print(" - {} -> {}".format(src_path, dest_path))
copy_tree(content_folder, destination) copy(str(src_path), str(dest_path))

View File

@@ -4,9 +4,10 @@ set -e
worker() { worker() {
WORKERS_COUNT=${WORKERS_COUNT:-2} WORKERS_COUNT=${WORKERS_COUNT:-2}
QUEUES=${QUEUES:-queries,scheduled_queries,celery,schemas} QUEUES=${QUEUES:-queries,scheduled_queries,celery,schemas}
WORKER_EXTRA_OPTIONS=${WORKER_EXTRA_OPTIONS:-}
echo "Starting $WORKERS_COUNT workers for queues: $QUEUES..." echo "Starting $WORKERS_COUNT workers for queues: $QUEUES..."
exec /usr/local/bin/celery worker --app=redash.worker -c$WORKERS_COUNT -Q$QUEUES -linfo --maxtasksperchild=10 -Ofair exec /usr/local/bin/celery worker --app=redash.worker -c$WORKERS_COUNT -Q$QUEUES -linfo --max-tasks-per-child=10 -Ofair $WORKER_EXTRA_OPTIONS
} }
scheduler() { scheduler() {
@@ -16,11 +17,24 @@ scheduler() {
echo "Starting scheduler and $WORKERS_COUNT workers for queues: $QUEUES..." echo "Starting scheduler and $WORKERS_COUNT workers for queues: $QUEUES..."
exec /usr/local/bin/celery worker --app=redash.worker --beat -s$SCHEDULE_DB -c$WORKERS_COUNT -Q$QUEUES -linfo --maxtasksperchild=10 -Ofair exec /usr/local/bin/celery worker --app=redash.worker --beat -s$SCHEDULE_DB -c$WORKERS_COUNT -Q$QUEUES -linfo --max-tasks-per-child=10 -Ofair
}
dev_worker() {
WORKERS_COUNT=${WORKERS_COUNT:-2}
QUEUES=${QUEUES:-queries,scheduled_queries,celery,schemas}
SCHEDULE_DB=${SCHEDULE_DB:-celerybeat-schedule}
echo "Starting dev scheduler and $WORKERS_COUNT workers for queues: $QUEUES..."
exec watchmedo auto-restart --directory=./redash/ --pattern=*.py --recursive -- /usr/local/bin/celery worker --app=redash.worker --beat -s$SCHEDULE_DB -c$WORKERS_COUNT -Q$QUEUES -linfo --max-tasks-per-child=10 -Ofair
} }
server() { server() {
exec /usr/local/bin/gunicorn -b 0.0.0.0:5000 --name redash -w${REDASH_WEB_WORKERS:-4} redash.wsgi:app # Recycle gunicorn workers every n-th request. See http://docs.gunicorn.org/en/stable/settings.html#max-requests for more details.
MAX_REQUESTS=${MAX_REQUESTS:-1000}
MAX_REQUESTS_JITTER=${MAX_REQUESTS_JITTER:-100}
exec /usr/local/bin/gunicorn -b 0.0.0.0:5000 --name redash -w${REDASH_WEB_WORKERS:-4} redash.wsgi:app --max-requests $MAX_REQUESTS --max-requests-jitter $MAX_REQUESTS_JITTER
} }
create_db() { create_db() {
@@ -40,6 +54,7 @@ help() {
echo "server -- start Redash server (with gunicorn)" echo "server -- start Redash server (with gunicorn)"
echo "worker -- start Celery worker" echo "worker -- start Celery worker"
echo "scheduler -- start Celery worker with a beat (scheduler) process" echo "scheduler -- start Celery worker with a beat (scheduler) process"
echo "dev_worker -- start Celery worker with a beat (scheduler) process which picks up code changes and reloads"
echo "celery_healthcheck -- runs a Celery healthcheck. Useful for Docker's HEALTHCHECK mechanism." echo "celery_healthcheck -- runs a Celery healthcheck. Useful for Docker's HEALTHCHECK mechanism."
echo "" echo ""
echo "shell -- open shell" echo "shell -- open shell"
@@ -74,6 +89,10 @@ case "$1" in
shift shift
scheduler scheduler
;; ;;
dev_worker)
shift
dev_worker
;;
dev_server) dev_server)
export FLASK_DEBUG=1 export FLASK_DEBUG=1
exec /app/manage.py runserver --debugger --reload -h 0.0.0.0 exec /app/manage.py runserver --debugger --reload -h 0.0.0.0

View File

@@ -1,7 +1,9 @@
#!/bin/sh #!/bin/sh
set -o errexit # fail the build if any task fails
flake8 --version ; pip --version flake8 --version ; pip --version
# stop the build if there are Python syntax errors or undefined names # stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics

View File

@@ -1,8 +0,0 @@
#!/bin/bash
NAME=redash
VERSION=$(python ./manage.py version)
FULL_VERSION=$VERSION+b$CIRCLE_BUILD_NUM
FILENAME=$NAME.$FULL_VERSION.tar.gz
sed -ri "s/^__version__ = '([A-Za-z0-9.-]*)'/__version__ = '$FULL_VERSION'/" redash/__init__.py
tar -zcv -f $FILENAME --exclude="optipng*" --exclude=".git*" --exclude="*.pyc" --exclude="*.pyo" --exclude="venv" --exclude="node_modules" *

View File

@@ -1,7 +1,10 @@
{ {
"presets": [ "presets": [
["@babel/preset-env", { ["@babel/preset-env", {
"targets": "> 0.5%, last 2 versions, Firefox ESR, ie 11, not dead", "exclude": [
"@babel/plugin-transform-async-to-generator",
"@babel/plugin-transform-arrow-functions"
],
"useBuiltIns": "usage" "useBuiltIns": "usage"
}], }],
"@babel/preset-react" "@babel/preset-react"

View File

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

View File

@@ -1,23 +1,21 @@
module.exports = { module.exports = {
root: true, root: true,
extends: ["airbnb", "plugin:jest/recommended"], extends: ["airbnb", "plugin:compat/recommended"],
plugins: ["jest", "cypress"], plugins: ["jest", "compat", "no-only-tests"],
settings: { settings: {
"import/resolver": "webpack" "import/resolver": "webpack"
}, },
parser: "babel-eslint", parser: "babel-eslint",
env: { env: {
"jest/globals": true, browser: true,
"cypress/globals": true, node: true
"browser": true,
"node": true
}, },
rules: { rules: {
// allow debugger during development // allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, "no-debugger": process.env.NODE_ENV === "production" ? 2 : 0,
'no-param-reassign': 0, "no-param-reassign": 0,
'no-mixed-operators': 0, "no-mixed-operators": 0,
'no-underscore-dangle': 0, "no-underscore-dangle": 0,
"no-use-before-define": ["error", "nofunc"], "no-use-before-define": ["error", "nofunc"],
"prefer-destructuring": "off", "prefer-destructuring": "off",
"prefer-template": "off", "prefer-template": "off",
@@ -27,34 +25,42 @@ module.exports = {
"no-lonely-if": "off", "no-lonely-if": "off",
"consistent-return": "off", "consistent-return": "off",
"no-control-regex": "off", "no-control-regex": "off",
'no-multiple-empty-lines': 'warn', "no-multiple-empty-lines": "warn",
"no-script-url": "off", // some <a> tags should have href="javascript:void(0)" "no-only-tests/no-only-tests": "error",
'operator-linebreak': 'off', "operator-linebreak": "off",
'react/destructuring-assignment': 'off', "react/destructuring-assignment": "off",
"react/jsx-filename-extension": "off", "react/jsx-filename-extension": "off",
'react/jsx-one-expression-per-line': 'off', "react/jsx-one-expression-per-line": "off",
"react/jsx-uses-react": "error", "react/jsx-uses-react": "error",
"react/jsx-uses-vars": "error", "react/jsx-uses-vars": "error",
'react/jsx-wrap-multilines': 'warn', "react/jsx-wrap-multilines": "warn",
'react/no-access-state-in-setstate': 'warn', "react/no-access-state-in-setstate": "warn",
"react/prefer-stateless-function": "warn", "react/prefer-stateless-function": "warn",
"react/forbid-prop-types": "warn", "react/forbid-prop-types": "warn",
"react/prop-types": "warn", "react/prop-types": "warn",
"jsx-a11y/anchor-is-valid": "off", "jsx-a11y/anchor-is-valid": "off",
"jsx-a11y/click-events-have-key-events": "off", "jsx-a11y/click-events-have-key-events": "off",
"jsx-a11y/label-has-associated-control": ["warn", { "jsx-a11y/label-has-associated-control": [
"controlComponents": true "warn",
}], {
controlComponents: true
}
],
"jsx-a11y/label-has-for": "off", "jsx-a11y/label-has-for": "off",
"jsx-a11y/no-static-element-interactions": "off", "jsx-a11y/no-static-element-interactions": "off",
"max-len": ['error', 120, 2, { "max-len": [
ignoreUrls: true, "error",
ignoreComments: false, 120,
ignoreRegExpLiterals: true, 2,
ignoreStrings: true, {
ignoreTemplateLiterals: true, ignoreUrls: true,
}], ignoreComments: false,
"no-else-return": ["error", {"allowElseIf": true}], ignoreRegExpLiterals: true,
"object-curly-newline": ["error", {"consistent": true}], ignoreStrings: true,
ignoreTemplateLiterals: true
}
],
"no-else-return": ["error", { allowElseIf: true }],
"object-curly-newline": ["error", { consistent: true }]
} }
}; };

10
client/app/.eslintrc.js Normal file
View File

@@ -0,0 +1,10 @@
module.exports = {
extends: ["plugin:jest/recommended"],
plugins: ["jest"],
env: {
"jest/globals": true,
},
rules: {
"jest/no-focused-tests": "off",
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -20,7 +20,10 @@
@import '~antd/lib/tag/style/index'; @import '~antd/lib/tag/style/index';
@import '~antd/lib/grid/style/index'; @import '~antd/lib/grid/style/index';
@import '~antd/lib/switch/style/index'; @import '~antd/lib/switch/style/index';
@import '~antd/lib/empty/style/index';
@import '~antd/lib/drawer/style/index'; @import '~antd/lib/drawer/style/index';
@import '~antd/lib/card/style/index';
@import '~antd/lib/steps/style/index';
@import '~antd/lib/divider/style/index'; @import '~antd/lib/divider/style/index';
@import '~antd/lib/dropdown/style/index'; @import '~antd/lib/dropdown/style/index';
@import '~antd/lib/menu/style/index'; @import '~antd/lib/menu/style/index';
@@ -29,8 +32,28 @@
@import "~antd/lib/card/style/index"; @import "~antd/lib/card/style/index";
@import "~antd/lib/spin/style/index"; @import "~antd/lib/spin/style/index";
@import "~antd/lib/tabs/style/index"; @import "~antd/lib/tabs/style/index";
@import "~antd/lib/notification/style/index";
@import "~antd/lib/collapse/style/index";
@import "~antd/lib/progress/style/index";
@import "~antd/lib/typography/style/index";
@import 'inc/ant-variables'; @import 'inc/ant-variables';
// Increase z-indexes to avoid conflicts with some other libraries (e.g. Plotly)
@zindex-modal: 2000;
@zindex-modal-mask: 2000;
@zindex-message: 2010;
@zindex-notification: 2010;
@zindex-popover: 2030;
@zindex-dropdown: 2050;
@zindex-picker: 2050;
@zindex-tooltip: 2060;
.@{drawer-prefix-cls} {
&.help-drawer {
z-index: @zindex-tooltip; // help drawer should be topmost
}
}
// Remove bold in labels for Ant checkboxes and radio buttons // Remove bold in labels for Ant checkboxes and radio buttons
.ant-checkbox-wrapper, .ant-checkbox-wrapper,
.ant-radio-wrapper { .ant-radio-wrapper {
@@ -54,11 +77,6 @@
} }
} }
// Fix for Ant dropdowns when they are used in Boootstrap modals
.ant-dropdown-in-bootstrap-modal {
z-index: 1050;
}
// Button overrides // Button overrides
.@{btn-prefix-cls} { .@{btn-prefix-cls} {
transition-duration: 150ms; transition-duration: 150ms;
@@ -132,6 +150,10 @@
border-color: transparent; border-color: transparent;
color: @pagination-color; color: @pagination-color;
line-height: @pagination-item-size - 2px; line-height: @pagination-item-size - 2px;
.@{pagination-prefix-cls}.mini & {
line-height: @pagination-item-size-sm - 2px;
}
} }
&:focus .@{pagination-prefix-cls}-item-link, &:focus .@{pagination-prefix-cls}-item-link,
@@ -217,21 +239,61 @@
} }
} }
// styling for short modals (no lines) .@{dialog-prefix-cls} {
.@{dialog-prefix-cls}.shortModal { // styling for short modals (no lines)
.@{dialog-prefix-cls} { &.shortModal {
&-header, .@{dialog-prefix-cls} {
&-footer { &-header,
border: none; &-footer {
padding: 16px; border: none;
padding: 16px;
}
&-body {
padding: 10px 16px;
}
&-close-x {
width: 46px;
height: 46px;
line-height: 46px;
}
} }
&-body { }
padding: 10px 16px;
} // fullscreen modals
&-close-x { &-fullscreen {
width: 46px; .@{dialog-prefix-cls} {
height: 46px; position: absolute;
line-height: 46px; left: 15px;
top: 15px;
right: 15px;
bottom: 15px;
width: auto !important;
height: auto !important;
max-width: none;
max-height: none;
margin: 0;
padding: 0;
.@{dialog-prefix-cls}-content {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
width: auto;
height: auto;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
.@{dialog-prefix-cls}-body {
flex: 1 1 auto;
overflow: auto;
}
} }
} }
} }
@@ -244,6 +306,60 @@
margin-top: 4px; margin-top: 4px;
} }
.ant-popover { // Notification overrides
z-index: 1000; // make sure it doesn't cover drawer .@{notification-prefix-cls} {
// vertical centering
&-notice-close {
top: 20px;
right: 20px;
}
&-notice-description {
max-width: 484px;
}
} }
.@{btn-prefix-cls} .@{iconfont-css-prefix}-ellipsis {
margin: 0 -7px;
}
// Collapse
.@{collapse-prefix-cls} {
&&-headerless {
border: 0;
background: none;
.@{collapse-prefix-cls}-header {
display: none;
}
.@{collapse-prefix-cls}-item,
.@{collapse-prefix-cls}-content {
border: 0;
}
.@{collapse-prefix-cls}-content-box {
padding: 0;
}
}
}
// overrides for tall form components such as ace editor
.@{form-prefix-cls}-item {
&-children {
display: block; // so feeback icon positions correctly
}
// no change for short components, sticks to body for tall ones
&-children-icon {
top: auto !important;
bottom: 8px;
// makes the icon white instead of see-through
& svg {
background: white;
border-radius: 50%;
}
}
}

View File

@@ -1,6 +1,5 @@
.alert { .alert {
padding-left: 30px; padding: 15px;
padding-right: 30px;
span { span {
cursor: pointer; cursor: pointer;

View File

@@ -19,6 +19,12 @@
@font-size-base: 13px; @font-size-base: 13px;
/* --------------------------------------------------------
Borders
-----------------------------------------------------------*/
@border-color-split: #f0f0f0;
/* -------------------------------------------------------- /* --------------------------------------------------------
Typograpgy Typograpgy
-----------------------------------------------------------*/ -----------------------------------------------------------*/
@@ -72,3 +78,9 @@
@table-row-hover-bg: fade(@redash-gray, 5%); @table-row-hover-bg: fade(@redash-gray, 5%);
@table-padding-vertical: 7px; @table-padding-vertical: 7px;
@table-padding-horizontal: 10px; @table-padding-horizontal: 10px;
/* --------------------------------------------------------
Notification
-----------------------------------------------------------*/
@notification-padding: @notification-padding-vertical 48px @notification-padding-vertical 17px;
@notification-width: auto;

View File

@@ -19,21 +19,30 @@ html, body {
} }
body { body {
padding-top: @header-height; padding-top: 0;
background: #F6F8F9;
font-family: @redash-font;
position: relative; position: relative;
padding-bottom: @footer-height;
&.headless { &.headless {
padding-top: 0; padding-top: 10px;
padding-bottom: 0;
.nav.app-header { .nav.app-header, .navbar {
display: none;
}
div#footer {
display: none; display: none;
} }
} }
} }
app-view {
min-height: 100vh;
}
app-view, #app-content {
display: flex;
flex-direction: column;
flex-grow: 1;
}
strong { strong {
font-weight: 500; font-weight: 500;
} }
@@ -42,20 +51,20 @@ strong {
position: relative; position: relative;
padding-top: 30px; padding-top: 30px;
padding-bottom: 30px; padding-bottom: 30px;
@media (min-width: (@screen-sm-min + 1)) { @media (min-width: (@screen-sm-min + 1)) {
padding-right: 15px; padding-right: 15px;
padding-left: 15px; padding-left: 15px;
} }
@media (min-width: (@screen-lg-min + 80px)) { @media (min-width: (@screen-lg-min + 80px)) {
margin-left: @sidebar-left-width; margin-left: @sidebar-left-width;
} }
@media (min-width: @screen-sm-min) and (max-width: (@screen-md-max + 80px)) { @media (min-width: @screen-sm-min) and (max-width: (@screen-md-max + 80px)) {
margin-left: @sidebar-left-mid-width; margin-left: @sidebar-left-mid-width;
} }
@media (max-width: (@screen-sm-min)) { @media (max-width: (@screen-sm-min)) {
margin-left: 0; margin-left: 0;
} }
@@ -67,10 +76,34 @@ strong {
} }
} }
// Fixed width layout for specific pages
@media (min-width: 768px) {
settings-screen, home-page, page-dashboard-list, page-queries-list, page-alerts-list, alert-page, queries-search-results-page, .fixed-container {
.container {
width: 750px;
}
}
}
@media (min-width: 992px) {
settings-screen, home-page, page-dashboard-list, page-queries-list, page-alerts-list, alert-page, queries-search-results-page, .fixed-container {
.container {
width: 970px;
}
}
}
@media (min-width: 1200px) {
settings-screen, home-page, page-dashboard-list, page-queries-list, page-alerts-list, alert-page, queries-search-results-page, .fixed-container {
.container {
width: 1170px;
}
}
}
.scrollbox { .scrollbox {
overflow: auto; overflow: auto;
position: relative; position: relative;
} }
.clickable { .clickable {
@@ -90,3 +123,150 @@ strong {
resize: both !important; resize: both !important;
transition: height 0s, width 0s !important; transition: height 0s, width 0s !important;
} }
// Ace Editor
.ace_editor {
border: 1px solid fade(@redash-gray, 15%) !important;
}
.ace-tm {
.ace_gutter {
background: #fff !important;
}
.ace_gutter-active-line {
background-color: fade(@redash-gray, 20%) !important;
}
.ace_marker-layer .ace_active-line {
background: fade(@redash-gray, 9%) !important;
}
}
.bg-ace {
background-color: fade(@redash-gray, 12%) !important;
}
// resizeable
.rg-top span, .rg-bottom span {
height: 3px;
border-color: #b1c1ce; // TODO: variable
}
.rg-bottom {
bottom: 15px;
span {
margin: 1.5px 0 0 -10px;
}
}
// Plotly
text.slicetext {
text-shadow: 1px 1px 5px #333;
}
// markdown
.markdown strong {
font-weight: bold;
}
.markdown img {
max-width: 100%;
}
.dropdown-menu > li > a:hover, .dropdown-menu > li > a:focus {
background-color: fade(@redash-gray, 15%);
color: #111;
}
.profile__image--navbar {
border-radius: 100%;
margin-right: 3px;
margin-top: -2px;
}
.profile__image--settings {
border-radius: 100%;
}
.profile__image_thumb {
border-radius: 100%;
margin-right: 3px;
margin-top: -2px;
width: 20px;
height: 20px;
}
// Error state
.error-state {
display: flex;
flex-direction: column;
justify-content: flex-start;
text-align: center;
margin-top: 25vh;
padding: 35px;
font-size: 14px;
line-height: 21px;
.error-state__icon {
.zmdi {
font-size: 64px;
color: @redash-gray;
}
}
@media (max-width: 767px) {
margin-top: 10vh;
}
}
// page
.page-header--new .btn-favourite, .page-header--new .btn-archive {
font-size: 19px;
}
.page-title {
display: flex;
align-items: center;
h3 {
margin-right: 5px !important;
}
.label {
margin-top: 3px;
display: inline-block;
}
favorites-control {
margin-right: 5px;
}
@media (max-width: 767px) {
display: block;
favorites-control {
float: left;
}
h3 {
width: 100%;
margin-bottom: 5px !important;
display: block !important;
}
}
}
.page-header-wrapper, .page-header--new {
h3 {
margin: 0.2em 0;
line-height: 1.3;
font-weight: 500;
}
}
.select-option-divider {
margin: 10px 0 !important;
}

View File

@@ -31,7 +31,7 @@
.collapsing, .collapsing,
.collapse.in { .collapse.in {
padding: 5px 10px; padding: 0;
transition: all 0.35s ease; transition: all 0.35s ease;
} }

View File

@@ -122,3 +122,21 @@
top: 1px; top: 1px;
position: relative; position: relative;
} }
.btn-default {
background-color: fade(@redash-gray, 15%);
}
.btn-transparent {
background-color: transparent !important;
}
.btn-default:hover, .btn-default:focus, .btn-default.focus, .btn-default:active, .btn-default.active, .open > .dropdown-toggle.btn-default {
background-color: fade(@redash-gray, 25%);
}
.btn-default:active:hover, .btn-default.active:hover, .open > .dropdown-toggle.btn-default:hover, .btn-default:active:focus, .btn-default.active:focus, .open > .dropdown-toggle.btn-default:focus, .btn-default:active.focus, .btn-default.active.focus, .open > .dropdown-toggle.btn-default.focus {
color: #333;
background-color: fade(@redash-gray, 45%);
}

View File

@@ -1,48 +0,0 @@
#footer {
position: absolute;
bottom: 0;
text-align: center;
width: 100%;
height: @footer-height;
color: #a2a2a2;
padding-top: 10px;
padding-bottom: 15px;
.f-menu {
display: block;
width: 100%;
.list-inline();
margin-top: 8px;
& > li > a {
color: #a2a2a2;
&:hover {
color: #777;
}
}
}
@media (min-width: (@screen-lg-min + 80px)) {
padding-left: (@sidebar-left-width + @grid-gutter-width);
}
@media (min-width: @screen-sm-min) and (max-width: (@screen-md-max + 80px)) {
padding-left: (@sidebar-left-mid-width + @grid-gutter-width);
}
@media (max-width: (@screen-sm-min)) {
padding-left: @grid-gutter-width/2;
}
}
.footer {
color: #818d9f;
padding-bottom: 30px;
a {
color: #818d9f;
margin-left: 20px;
}
}

View File

@@ -55,14 +55,17 @@ textarea.v-resizable {
.transition-duration(300ms); .transition-duration(300ms);
resize: none; resize: none;
box-shadow: 0 0 0 40px rgba(0, 0, 0, 0) !important; box-shadow: 0 0 0 40px rgba(0, 0, 0, 0) !important;
border-radius: 0; border-radius: @redash-input-radius;
&:focus { &:focus {
box-shadow: 0 0 1px -2px rgba(121,194,255,0.5) !important; box-shadow: none !important;
border-color: @blue;
}
&:hover {
border-color: @blue;
} }
} }
/* -------------------------------------------------------- /* --------------------------------------------------------
Custom Checkbox + Radio Custom Checkbox + Radio
-----------------------------------------------------------*/ -----------------------------------------------------------*/

View File

@@ -153,4 +153,10 @@
/* -------------------------------------------------------- /* --------------------------------------------------------
Border Radius Border Radius
-----------------------------------------------------------*/ -----------------------------------------------------------*/
.brd-2 { border-radius: 2px; } .brd-2 { border-radius: 2px; }
/* --------------------------------------------------------
Alignment
-----------------------------------------------------------*/
.va-top { vertical-align: top; }

View File

@@ -1,14 +1,37 @@
.label { .label {
border-radius: 1px; border-radius: 2px;
padding: 4px 5px 3px; padding: 3px 6px 4px;
} font-weight: 500;
font-size: 11px;
h1, h2, h3, h4, h5, h6 {
.label {
border-radius: 2px;
}
} }
.badge { .badge {
border-radius: 1px; border-radius: 1px;
}
.label-default {
background: fade(@redash-gray, 85%);
}
.label-tag-unpublished {
background: fade(@redash-gray, 85%);
}
.label-tag-archived {
.label-warning();
}
.label-tag {
background: fade(@redash-gray, 10%);
color: fade(@redash-gray, 75%);
}
.label-tag-unpublished,
.label-tag-archived,
.label-tag {
margin-right: 3px;
display: inline;
margin-top: 2px;
max-width: 24ch;
.text-overflow();
} }

View File

@@ -31,6 +31,17 @@ tags-list {
line-height: 1.3; line-height: 1.3;
} }
.tags-list {
.badge-light {
background: fade(@redash-gray, 10%);
color: fade(@redash-gray, 75%);
}
a:hover {
cursor: pointer;
}
}
.max-character { .max-character {
.text-overflow(); .text-overflow();
} }
@@ -45,6 +56,11 @@ tags-list {
line-height: 100%; line-height: 100%;
margin-top: 2px; margin-top: 2px;
} }
&.active, &.active:hover, &.active:focus {
background-color: #fff;
box-shadow: inset 3px 0px 0px @brand-primary;
}
} }
.list-group-item-heading { .list-group-item-heading {
@@ -76,3 +92,18 @@ tags-list {
height: 38px; height: 38px;
border-radius: 2px; border-radius: 2px;
} }
.ui-select-choices-row.disabled > span {
background-color: inherit !important;
}
.list-group-item.inactive,
.ui-select-choices-row.disabled {
background-color: #eee !important;
border-color: transparent;
opacity: 0.5;
box-shadow: none;
color: #333;
pointer-events: none;
cursor: not-allowed;
}

View File

@@ -225,4 +225,13 @@
height: 37px; height: 37px;
border-radius: 2px; border-radius: 2px;
width: 37px; width: 37px;
}
/* --------------------------------------------------------
Percy
-----------------------------------------------------------*/
@media only percy {
.hide-in-percy, .pace {
visibility: hidden;
}
} }

View File

@@ -30,3 +30,266 @@ a.navbar-brand img {
left: -9px; left: -9px;
bottom: -11px; bottom: -11px;
} }
.caret--nav {
border-top: none;
}
.caret--nav:after {
content: "";
position: absolute;
right: 5px;
top: 9px;
width: 13px;
height: 13px;
display: block;
background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='11px' height='6px' viewBox='0 0 11 6' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3C!-- Generator: Sketch 42 %2836781%29 - http://www.bohemiancoding.com/sketch --%3E%3Ctitle%3EShape%3C/title%3E%3Cdesc%3ECreated with Sketch.%3C/desc%3E%3Cdefs%3E%3C/defs%3E%3Cg id='Page-1' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cpath d='M5.296,4.288 L9.382,0.2 C9.66086822,-0.0716916976 10.1065187,-0.068122925 10.381,0.208 C10.661,0.488 10.661,0.932 10.388,1.206 L5.792,5.803 C5.6602899,5.93388911 5.48167943,6.00662966 5.296,6.005 C5.10997499,6.00689786 4.93095449,5.93413702 4.799,5.803 L0.204,1.207 C0.072163111,1.07394937 -0.00121750401,0.893846387 9.62313189e-05,0.706545264 C0.00140996665,0.519244142 0.0773097323,0.340188219 0.211,0.209 C0.485365732,-0.0664648737 0.930253538,-0.0700311086 1.209,0.201 L5.296,4.288 L5.296,4.288 Z' id='Shape' fill='%23000000'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
background-size: 100% 100%;
transition: transform .2s cubic-bezier(.75,0,.25,1);
}
.navbar .caret--nav:after {
top: 19px;
}
.dropdown--profile .caret--nav:after {
right: 8px;
}
.btn--create {
padding-right: 20px;
.caret--nav:after {
top: 10px;
right: 10px;
background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='11px' height='6px' viewBox='0 0 11 6' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3C!-- Generator: Sketch 42 %2836781%29 - http://www.bohemiancoding.com/sketch --%3E%3Ctitle%3EShape%3C/title%3E%3Cdesc%3ECreated with Sketch.%3C/desc%3E%3Cdefs%3E%3C/defs%3E%3Cg id='Page-1' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cpath d='M5.29592111,4.28945339 L9.38192111,0.201453387 C9.66078932,-0.0702383105 10.1064398,-0.0666695379 10.3809211,0.209453387 C10.6609211,0.489453387 10.6609211,0.933453387 10.3879211,1.20745339 L5.79192111,5.80445339 C5.66021101,5.9353425 5.48160054,6.00808305 5.29592111,6.00645339 C5.1098961,6.00835125 4.9308756,5.9355904 4.79892111,5.80445339 L0.203921109,1.20845339 C0.0720842204,1.07540275 -0.00129639464,0.895299774 1.73406884e-05,0.707998651 C0.00133107602,0.520697529 0.0772308417,0.341641606 0.210921109,0.210453387 C0.485286842,-0.0650114866 0.930174648,-0.0685777215 1.20892111,0.202453387 L5.29592111,4.28945339 L5.29592111,4.28945339 Z' id='Shape' fill='%23FCFCFC'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
}
}
.dropdown.open .caret--nav:after {
transform: rotate(180deg);
}
.navbar {
box-shadow: fade(@redash-gray, 15%) 0px 4px 9px -3px;
.navbar-collapse {
padding-left: 0;
}
a.dropdown--profile {
padding-top: 10px;
padding-bottom: 10px;
line-height: 2.35;
}
.navbar-inverse {
background-color: @redash-gray;
border: none;
}
}
.navbar-btn {
margin-top: 10px;
margin-bottom: 9px;
}
.navbar-brand {
position: absolute;
left: 50%;
margin-left: -25px !important; // center
display: block;
zoom: 0.9;
}
.menu-search {
margin-top: 2px;
}
.dropdown-menu--profile {
li {
width: 200px;
}
}
.navbar .collapse.in {
background: #fff;
position: relative;
z-index: 999;
padding: 0 10px 0 10px;
}
.navbar {
min-height: initial;
height: 50px;
border: 1px solid #fff;
border-top: none;
border-radius: 0;
background: #fff;
margin-bottom: 10px;
.btn-group.open .dropdown-toggle {
-webkit-box-shadow: none;
box-shadow: none;
}
.btn-group .btn:active {
box-shadow: none;
}
}
.navbar-link-ANGULAR_REMOVE_ME {
line-height: 18px;
padding: 10px 15px;
display: block;
@media (min-width: 768px) {
padding-top: 16px;
padding-bottom: 16px;
}
}
.navbar-link-ANGULAR_REMOVE_ME,
.navbar-default .navbar-nav > li > a {
color: #000;
font-weight: 500;
&:active, &:hover, &:focus {
color: #000;
}
}
.navbar-default .btn__new button {
font-weight: 500;
}
.btn__new {
margin-left: 15px;
}
.navbar-default .navbar-nav > li > a:hover {
//background-color: fade(@redash-gray, 10%);
//text-decoration: underline;
//border-radius: 0;
}
.navbar-default .navbar-nav > .open > a, .navbar-default .navbar-nav > .open > a:hover, .navbar-default .navbar-nav > .open > a:focus {
background-color: fade(@redash-gray, 15%);
color: #111;
}
// Responsive fixes
@media (max-width: 767px) {
.navbar-brand {
left: 2%;
margin-left: 0 !important;
}
//Fix navbar collapse
.navbar .collapse.in {
border: none;
.dropdown-menu--profile {
li {
width: auto;
}
}
.dropdown--profile {
.caret--nav:after {
right: initial !important;
}
}
.dropdown--profile__username {
display: inline-block;
}
.nav__main li a {
padding: 10px 15px;
display: block;
text-align: left;
float: none !important;
}
.navbar-form {
margin-bottom: 0;
margin-top: 0;
}
.navbar-right {
margin-bottom: 0;
}
}
}
@media (min-width: 768px) {
@media (max-width: 880px) {
.navbar-link-ANGULAR_REMOVE_ME,
.navbar-default .navbar-nav > li > a,
.navbar-form {
padding-left: 10px !important;
padding-right: 10px !important;
}
a.navbar-brand {
margin-left: -15px !important;
}
}
@media (max-width: 810px) {
.menu-search {
width: 175px;
}
a.navbar-brand {
margin-left: 13px !important;
}
}
}
@media (max-width: 1084px) {
.dropdown--profile__username {
display: none;
}
}
// Cross-browser fixes
// Firefox
@-moz-document url-prefix() {
.caret--nav::after {
height: 7px;
}
.navbar .caret--nav::after {
top: 22px;
}
.navbar .btn--create .caret--nav::after {
top: 12px;
}
}
// IE10+
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
.caret--nav::after {
height: 7px;
}
.navbar .caret--nav::after {
top: 22px;
}
.navbar .btn--create .caret--nav::after {
top: 12px;
}
}
.navbar li a .btn-favourite .fa, .navbar li a .btn-archive .fa {
font-size: 100%;
}

View File

@@ -1,54 +0,0 @@
.pagination {
border-radius: 0;
& > li {
margin: 0 2px;
display: inline-block;
vertical-align: top;
& > a,
& > span {
border-radius: 50% !important;
padding: 0;
width: 40px;
height: 40px;
line-height: 38px;
text-align: center;
font-size: 14px;
z-index: 1;
position: relative;
& > .zmdi {
font-size: 22px;
line-height: 39px;
}
}
&.disabled {
.opacity(0.5);
}
}
}
/* --------------------------------------------------------
Listview Pagination
-----------------------------------------------------------*/
.lv-pagination {
width: 100%;
text-align: center;
padding: 40px 0;
border-top: 1px solid #F0F0F0;
margin-top: 0;
margin-bottom: 0;
}
/* --------------------------------------------------------
Pager
-----------------------------------------------------------*/
.pager li > a, .pager li > span {
padding: 5px 10px 6px;
color: @pagination-color;
}

View File

@@ -1,5 +1,5 @@
.popover { .popover {
box-shadow: 0 2px 30px rgba(0, 0, 0, 0.2); box-shadow: fade(@redash-gray, 25%) 0px 0px 15px 0px;
} }
.popover-title { .popover-title {

View File

@@ -12,7 +12,6 @@
#header, #header,
#footer,
#sidebar, #sidebar,
#chat, #chat,
.growl-animated, .growl-animated,

View File

@@ -132,9 +132,15 @@
} }
.tab-nav { .tab-nav {
margin-bottom: 0px;
> li.rd-tab-btn { > li.rd-tab-btn {
float: right; float: right;
padding-right: 10px; padding-right: 10px;
padding-top: 10px; padding-top: 10px;
} }
> li > a {
text-transform: capitalize;
}
} }

View File

@@ -1,26 +1,26 @@
.table { .table {
margin-bottom: 0; margin-bottom: 0;
th.sortable-column { th.sortable-column {
cursor: pointer; cursor: pointer;
} }
&:not(.table-striped) > thead > tr > th { &:not(.table-striped) > thead > tr > th {
background-color: #FAFAFA; background-color: #FAFAFA;
} }
[class*="bg-"] { [class*="bg-"] {
& > tr > th { & > tr > th {
color: #fff; color: #fff;
border-bottom: 0; border-bottom: 0;
background: transparent !important; background: transparent !important;
} }
& + tbody > tr:first-child > td { & + tbody > tr:first-child > td {
border-top: 0; border-top: 0;
} }
} }
& > thead > tr > th { & > thead > tr > th {
vertical-align: middle; vertical-align: middle;
font-weight: 500; font-weight: 500;
@@ -29,24 +29,24 @@
text-transform: uppercase; text-transform: uppercase;
padding: 15px 10px; padding: 15px 10px;
} }
& > thead > tr, & > thead > tr,
& > tbody > tr, & > tbody > tr,
& > tfoot > tr { & > tfoot > tr {
& > th, & > td { & > th, & > td {
&:first-child { &:first-child {
padding-left: 30px; padding-left: 30px;
} }
&:last-child { &:last-child {
padding-right: 30px; padding-right: 30px;
} }
} }
} }
tbody > tr:last-child > td { tbody > tr:last-child > td {
padding-bottom: 20px; padding-bottom: 20px;
} }
@@ -54,21 +54,21 @@
.table-bordered { .table-bordered {
border: 0; border: 0;
& > tbody > tr { & > tbody > tr {
& > td, & > th { & > td, & > th {
border-bottom: 0; border-bottom: 0;
border-left: 0; border-left: 0;
&:last-child { &:last-child {
border-right: 0; border-right: 0;
} }
} }
} }
& > thead > tr > th { & > thead > tr > th {
border-left: 0; border-left: 0;
&:last-child { &:last-child {
border-right: 0; border-right: 0;
} }
@@ -86,14 +86,64 @@
} }
.tile .table { .tile .table {
& > thead:not([class*="bg-"]) > tr > th { & > thead:not([class*="bg-"]) > tr > th {
border-top: 1px solid @table-border-color; border-top: 1px solid @table-border-color;
} }
} }
.table-hover > tbody > tr:hover { .table-hover > tbody > tr:hover {
background-color: #f4f4f4; background-color: #f4f4f4;
} }
.table-data {
tbody > tr > td {
padding-top: 5px !important;
}
.btn-favourite, .btn-archive {
font-size: 15px;
}
}
.table-main-title {
font-weight: 500;
line-height: 1.7 !important;
}
.btn-favourite {
color: #d4d4d4;
transition: all .25s ease-in-out;
&:hover, &:focus {
color: @yellow-darker;
cursor: pointer;
}
.fa-star {
color: @yellow-darker;
}
}
.btn-archive {
color: #d4d4d4;
transition: all .25s ease-in-out;
&:hover, &:focus {
color: @gray-light;
}
.fa-archive {
color: @gray-light;
}
}
.table > thead > tr > th {
text-transform: none;
}
.table-data .label-tag {
display: inline-block;
max-width: 135px;
}

View File

@@ -2,7 +2,8 @@
background-color: #fff; background-color: #fff;
margin-bottom: @grid-gutter-width; margin-bottom: @grid-gutter-width;
position: relative; position: relative;
box-shadow: @tile-shadow; border-radius: 3px;
box-shadow: fade(@redash-gray, 15%) 0px 4px 9px -3px;
&[class*="bg-"] { &[class*="bg-"] {
color: #fff; color: #fff;
@@ -12,6 +13,10 @@
margin-bottom: @grid-gutter-width/2; margin-bottom: @grid-gutter-width/2;
} }
} }
.tiled {
border-radius: 3px;
box-shadow: fade(@redash-gray, 15%) 0px 4px 9px -3px;
}
.t-header { .t-header {
.th-title { .th-title {
@@ -74,6 +79,15 @@
} }
} }
.t-header:not(.th-alt) {
padding: 15px;
ul {
margin-bottom: 0;
line-height: 2.2;
}
}
.tb-padding { .tb-padding {
padding: 20px 23px 30px; padding: 20px 23px 30px;
} }

View File

@@ -17,13 +17,14 @@
Template Variables Template Variables
-----------------------------------------------------------*/ -----------------------------------------------------------*/
@header-height: 60px; @header-height: 60px;
@footer-height: 95px;
@sidebar-left-width: 240px; @sidebar-left-width: 240px;
@sidebar-left-mid-width: 64px; @sidebar-left-mid-width: 64px;
@logo-width: @sidebar-left-width; @logo-width: @sidebar-left-width;
@logo-height: @header-height; @logo-height: @header-height;
@boxed-width: 1170px; @boxed-width: 1170px;
@body-bg: #edecec; @body-bg: #edecec;
@spacing: 15px;
@redash-radius: 3px;
/* -------------------------------------------------------- /* --------------------------------------------------------
@@ -40,6 +41,7 @@
-----------------------------------------------------------*/ -----------------------------------------------------------*/
@font-icon: 'Material-Design-Iconic-Font'; @font-icon: 'Material-Design-Iconic-Font';
@font-family-sans-serif: 'Roboto', sans-serif; @font-family-sans-serif: 'Roboto', sans-serif;
@redash-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
@font-size-base: 13px; @font-size-base: 13px;
@@ -60,6 +62,7 @@
@input-border: #e8e8e8; @input-border: #e8e8e8;
@input-border-radius: 0; @input-border-radius: 0;
@input-border-radius-large: 0px; @input-border-radius-large: 0px;
@redash-input-radius: 2px;
@input-height-large: 40px; @input-height-large: 40px;
@input-height-base: 35px; @input-height-base: 35px;
@input-height-small: 30px; @input-height-small: 30px;
@@ -95,6 +98,11 @@
@gray-light: #828282; @gray-light: #828282;
@ace: #f8f8f8; @ace: #f8f8f8;
@redash-gray: rgba(102, 136, 153, 1);
@redash-orange: rgba(255, 120, 100, 1);
@redash-black: rgba(0, 0, 0, 1);
@redash-yellow: rgba(252, 252, 161, 0.75);
/** Form States **/ /** Form States **/
@state-success-text: @green; @state-success-text: @green;
@state-info-text: @blue; @state-info-text: @blue;
@@ -193,7 +201,6 @@
@pagination-hover-color: #333; @pagination-hover-color: #333;
@pagination-hover-bg: #d7d7d7; @pagination-hover-bg: #d7d7d7;
@pagination-hover-border: @pagination-border; @pagination-hover-border: @pagination-border;
@pager-border-radius: 5px;
/* -------------------------------------------------------- /* --------------------------------------------------------

View File

@@ -1,45 +0,0 @@
counter-renderer {
display: block;
text-align: center;
padding: 15px 10px;
overflow: hidden;
counter {
margin: 0;
padding: 0;
font-size: 80px;
line-height: normal;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
value,
counter-target {
font-size: 1em;
display: block;
}
counter-name {
font-size: 0.5em;
display: block;
}
&.positive value {
color: #5cb85c;
}
&.negative value {
color: #d9534f;
}
}
counter-target {
color: #ccc;
}
counter-name {
font-size: 0.5em;
display: block;
}
}

View File

@@ -1,4 +1,6 @@
visualization-renderer { visualization-renderer {
display: block;
.pagination, .pagination,
.ant-pagination { .ant-pagination {
margin: 0; margin: 0;

View File

@@ -1,3 +1,4 @@
pivot-table-renderer > table, grid-renderer > div, visualization-renderer > div { .pivot-table-renderer > table,
visualization-renderer > .visualization-renderer-wrapper {
overflow: auto; overflow: auto;
} }

View File

@@ -7,7 +7,6 @@
/** Load Vendors Dependencies **/ /** Load Vendors Dependencies **/
@import '~font-awesome/less/font-awesome'; @import '~font-awesome/less/font-awesome';
@import '~ui-select/dist/select.css'; @import '~ui-select/dist/select.css';
@import '~angular-toastr/src/toastr';
@import '~angular-resizable/src/angular-resizable.css'; @import '~angular-resizable/src/angular-resizable.css';
@import '~material-design-iconic-font/dist/css/material-design-iconic-font.css'; @import '~material-design-iconic-font/dist/css/material-design-iconic-font.css';
@import '~pace-progress/themes/blue/pace-theme-minimal.css'; @import '~pace-progress/themes/blue/pace-theme-minimal.css';
@@ -33,7 +32,6 @@
@import 'inc/progress-bar'; @import 'inc/progress-bar';
@import 'inc/widgets'; @import 'inc/widgets';
@import 'inc/table'; @import 'inc/table';
@import 'inc/pagination';
@import 'inc/alert'; @import 'inc/alert';
@import 'inc/media'; @import 'inc/media';
@import 'inc/modal'; @import 'inc/modal';
@@ -45,7 +43,6 @@
@import 'inc/jumbotron'; @import 'inc/jumbotron';
@import 'inc/profile'; @import 'inc/profile';
@import 'inc/404'; @import 'inc/404';
@import 'inc/footer';
@import 'inc/ie-warning'; @import 'inc/ie-warning';
@import 'inc/navbar'; @import 'inc/navbar';
@import 'inc/edit-in-place'; @import 'inc/edit-in-place';
@@ -56,11 +53,9 @@
@import 'inc/schema-browser'; @import 'inc/schema-browser';
@import 'inc/toast'; @import 'inc/toast';
@import 'inc/visualizations/box'; @import 'inc/visualizations/box';
@import 'inc/visualizations/counter-render';
@import 'inc/visualizations/sankey'; @import 'inc/visualizations/sankey';
@import 'inc/visualizations/pivot-table'; @import 'inc/visualizations/pivot-table';
@import 'inc/visualizations/map'; @import 'inc/visualizations/map';
@import 'inc/visualizations/chart';
@import 'inc/visualizations/sunburst'; @import 'inc/visualizations/sunburst';
@import 'inc/visualizations/cohort'; @import 'inc/visualizations/cohort';
@import 'inc/visualizations/misc'; @import 'inc/visualizations/misc';
@@ -73,10 +68,11 @@
@import 'inc/vendor-overrides/ui-select'; @import 'inc/vendor-overrides/ui-select';
/** REDASH STYLING **/ /** REDASH STYLING **/
@import 'redash/redash-newstyle';
@import 'redash/redash-table'; @import 'redash/redash-table';
@import 'redash/query'; @import 'redash/query';
@import 'redash/tags-control'; @import 'redash/tags-control';
@import 'redash/css-logo';
@import 'redash/loading-indicator';

View File

@@ -0,0 +1,88 @@
// based on https://github.com/outbrain/tech-companies-logos-in-css/pull/28
@primary: #ff7964;
@shadow: #ef6c58;
@bar: white;
#css-logo {
width: 100px;
height: 100px;
position: relative;
#circle {
width: 79px;
height: 79px;
background-color: @shadow;
border-radius: 50%;
margin: auto;
overflow: hidden;
position: relative;
& > div {
width: 79px;
height: 73px;
background-color: @primary;
border-radius: 50%;
position: absolute;
top: 0;
}
}
#bars {
position: absolute;
left: 0;
top: 24px;
right: 0;
height: 33px;
display: flex;
padding: 0 22px 0;
.bar {
background: @bar;
box-shadow: 0px 2px 0 0 @shadow;
display: inline-block;
border-radius: 1px;
align-self: flex-end;
flex: 1;
margin: 0 2px;
border-radius: 3px;
&:nth-child(1) {
height: 32%;
}
&:nth-child(2) {
height: 71%;
}
&:nth-child(3) {
height: 50%;
}
&:nth-child(4) {
height: 100%;
}
}
}
#point,
#point > div {
position: absolute;
width: 0;
height: 0;
border: 17px solid @shadow;
border-right-color: transparent !important;
border-bottom-color: transparent !important;
bottom: 0;
left: 48px;
transform: scaleX(0.87);
transform-origin: left;
}
#point > div {
bottom: -12px;
border-color: @primary;
transform: scaleX(1.04);
left: -17px;
}
}

View File

@@ -0,0 +1,51 @@
.loading-indicator {
position: fixed;
top: 50%;
left: 50%;
margin: -50px 0 0 -50px; // center
width: 100px;
height: 100px;
transition-duration: 150ms;
transition-timing-function: linear;
transition-property: opacity, transform;
#css-logo {
animation: hover 2s infinite;
}
#shadow {
width: 33px;
height: 12px;
border-radius: 50%;
background-color: black;
opacity: 0.25;
display: block;
position: absolute;
left: 34px;
top: 115px;
animation: shadow 2s infinite;
}
@keyframes hover {
50% {
transform: translateY(-5px);
}
}
@keyframes shadow {
50% {
transform: scaleX(0.9);
opacity: 0.2;
}
}
}
// hide indicator when app-view has content
app-view:not(:empty) ~ .loading-indicator {
opacity: 0;
transform: scale(0.9);
pointer-events: none;
* {
animation: none !important;
}
}

View File

@@ -92,7 +92,7 @@ edit-in-place p.editable:hover {
} }
.filter-container { .filter-container {
margin-bottom: 10px; margin-bottom: 5px;
} }
.ace_editor.ace_autocomplete .ace_completion-highlight { .ace_editor.ace_autocomplete .ace_completion-highlight {
@@ -172,6 +172,12 @@ edit-in-place p.editable:hover {
} }
} }
.query-log-line {
font-family: monospace;
white-space: pre;
margin: 0;
}
.paginator-container { .paginator-container {
text-align: center; text-align: center;
} }
@@ -202,21 +208,21 @@ edit-in-place p.editable:hover {
} }
} }
.visualization-renderer {
.pagination,
.ant-pagination {
margin-top: 10px;
}
}
.embed__vis { .embed__vis {
display: flex; display: flex;
flex-flow: column; flex-flow: column;
} }
.embed-heading {
h3 {
line-height: 1.75;
margin: 0;
}
}
.widget-wrapper { .widget-wrapper {
.body-container { .body-container {
filters { .filters-wrapper {
display: block; display: block;
padding-left: 15px; padding-left: 15px;
} }
@@ -258,6 +264,7 @@ a.label-tag {
.query-page-wrapper { .query-page-wrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex-grow: 1;
} }
.query-fullscreen { .query-fullscreen {
@@ -336,7 +343,8 @@ a.label-tag {
border-bottom: 1px solid #efefef; border-bottom: 1px solid #efefef;
} }
pivot-table-renderer > table, grid-renderer > div, visualization-renderer > div { .pivot-table-renderer > table,
visualization-renderer > .visualization-renderer-wrapper {
overflow: visible; overflow: visible;
} }
@@ -551,6 +559,10 @@ nav .rg-bottom {
text-transform: capitalize; text-transform: capitalize;
} }
.edit-visualization {
margin-right: 5px;
}
// Smaller screens // Smaller screens
@media (max-width: 880px) { @media (max-width: 880px) {
@@ -665,8 +677,17 @@ nav .rg-bottom {
.filter-container { .filter-container {
padding-right: 0; padding-right: 0;
} }
}
.btn-edit-visualisation { // Responsive fixes
@media (max-width: 767px) {
.query-page-wrapper {
h3 {
font-size: 18px;
}
favorites-control {
margin-top: -3px;
}
} }
} }

View File

@@ -1,978 +0,0 @@
@import (reference, less) '~bootstrap/less/labels.less';
// Variables
@redash-gray: rgba(102, 136, 153, 1);
@redash-orange: rgba(255, 120, 100, 1);
@redash-black: rgba(0, 0, 0, 1);
@redash-yellow: rgba(252, 252, 161, 0.75);
@redash-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
@spacing: 15px;
//Default spacing (between tiles)
@redash-space: 10px;
@redash-radius: 3px;
@redash-input-radius: 2px;
// General
body {
padding-top: 0;
background: #F6F8F9;
font-family: @redash-font;
&.headless {
padding-top: 10px;
padding-bottom: 15px;
.navbar {
display: none !important;
}
div#footer {
display: none;
}
}
}
.word-wrap-break {
word-wrap: break-word;
}
.clearboth {
clear: both;
}
.callout {
padding: 20px;
border: 1px solid #eee;
border-left-width: 5px;
border-radius: 3px;
}
.callout-warning {
border-left-color: #aa6708;
}
.callout-info {
border-left-color: #1b809e;
}
// Fixed width layout for specific pages
@media (min-width: 768px) {
settings-screen, home-page, page-dashboard-list, page-queries-list, page-alerts-list, alert-page, queries-search-results-page, .fixed-container {
.container {
width: 750px;
}
}
}
@media (min-width: 992px) {
settings-screen, home-page, page-dashboard-list, page-queries-list, page-alerts-list, alert-page, queries-search-results-page, .fixed-container {
.container {
width: 970px;
}
}
}
@media (min-width: 1200px) {
settings-screen, home-page, page-dashboard-list, page-queries-list, page-alerts-list, alert-page, queries-search-results-page, .fixed-container {
.container {
width: 1170px;
}
}
}
.creation-container {
h5 {
color: #a7a7a7;
}
h3 {
margin: 0px;
margin-bottom: 15px;
}
}
.add-widget-container {
background: #fff;
border-radius: @redash-radius;
padding: 15px;
position: fixed;
left: 15px;
bottom: 20px;
width: calc(~'100% - 30px');
z-index: 99;
box-shadow: fade(@redash-gray, 50%) 0px 7px 29px -3px;
display: flex;
justify-content: space-between;
h2 {
margin: 0;
font-size: 14px;
line-height: 2.1;
font-weight: 400;
.zmdi {
margin: 0;
margin-right: 5px;
font-size: 24px;
position: absolute;
bottom: 18px;
}
span {
padding-left: 30px;
}
}
.btn {
align-self: center;
}
}
body {
.ace-tm .ace_gutter {
background: #fff;
}
.ace_editor {
border: 1px solid fade(@redash-gray, 15%);
}
.ace-tm .ace_gutter-active-line {
background-color: fade(@redash-gray, 20%);
}
.ace-tm .ace_marker-layer .ace_active-line {
background: fade(@redash-gray, 9%);
}
}
.list-group-item.active, .list-group-item.active:hover, .list-group-item.active:focus {
background-color: #fff;
box-shadow: inset 3px 0px 0px @brand-primary;
}
.table-data {
tbody > tr > td {
padding-top: 5px !important;
}
.btn-favourite, .btn-archive {
font-size: 15px;
}
}
.table-main-title {
font-weight: 500;
line-height: 1.7 !important;
a {
//font-size: 15px;
}
}
.btn-favourite, .btn-archive {
color: #d4d4d4;
transition: all .25s ease-in-out;
&:hover, &:focus {
color: @yellow-darker;
}
.fa-star {
color: @yellow-darker;
}
}
.btn-archive {
color: #d4d4d4;
transition: all .25s ease-in-out;
&:hover, &:focus {
color: @gray-light;
}
.fa-archive {
color: @gray-light;
}
}
.page-header--new .btn-favourite, .page-header--new .btn-archive {
font-size: 19px;
}
.page-title {
display: flex;
align-items: center;
h3 {
margin-right: 5px !important;
}
.label {
margin-top: 3px;
display: inline-block;
}
favorites-control {
margin-right: 5px;
}
@media (max-width: 767px) {
display: block;
favorites-control {
float: left;
}
h3 {
width: 100%;
margin-bottom: 5px !important;
display: block !important;
}
}
}
.navbar li a .btn-favourite .fa, .navbar li a .btn-archive .fa {
font-size: 100%;
}
.float-right {
float: right;
}
.database-source {
display: inline-flex;
flex-wrap: wrap;
justify-content: center;
}
.visual-card {
background: #FFFFFF;
border: 1px solid fade(@redash-gray, 15%);
border-radius: 3px;
margin: 5px;
width: 212px;
padding: 15px 5px;
cursor: pointer;
box-shadow: none;
transition: transform 0.12s ease-out;
transition-duration: 0.3s;
transition-property: box-shadow;
display: flex;
//flex-direction: row;
align-items: center;
&:hover {
box-shadow: rgba(102, 136, 153, 0.15) 0px 4px 9px -3px;
}
img {
width: 64px !important;
height: 64px !important;
margin-right: 5px;
}
h3 {
font-size: 13px;
color: #323232;
margin: 0 !important;
text-overflow: ellipsis;
overflow: hidden;
}
}
.visual-card--selected {
background: fade(@redash-gray, 3%);
border: 1px solid fade(@redash-gray, 15%);
border-radius: 3px;
padding: 0 15px;
box-shadow: none;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-around;
margin-bottom: 15px;
width: 100%;
img {
width: 64px;
height: 64px;
}
a {
cursor: pointer;
}
}
@media (max-width: 1200px) {
.visual-card {
width: 217px;
}
}
@media (max-width: 755px) {
.visual-card {
width: 47%;
}
}
@media (max-width: 515px) {
.visual-card {
width: 47%;
img {
width: 48px;
height: 48px;
}
}
}
@media (max-width: 408px) {
.visual-card {
width: 100%;
padding: 5px;
margin: 5px 0;
img {
width: 48px;
height: 48px;
}
}
}
.t-header:not(.th-alt) {
padding: 15px;
ul {
margin-bottom: 0;
line-height: 2.2;
}
}
#footer {
height: auto;
line-height: 3;
padding: 20px;
}
.page-header-wrapper, .page-header--new {
h3 {
margin: 0.2em 0;
line-height: 1.3;
font-weight: 500;
}
}
.alert {
padding: 15px;
}
.dynamic-table__pagination {
margin-top: 10px;
}
.rg-top span, .rg-bottom span {
height: 3px;
border-color: #b1c1ce; // TODO: variable
}
.rg-bottom {
bottom: 15px;
span {
margin: 1.5px 0 0 -10px;
}
}
.popover {
box-shadow: fade(@redash-gray, 25%) 0px 0px 15px 0px;
}
.tile__bottom-control a {
color: fade(@redash-black, 65%);
&:hover {
color: fade(@redash-black, 95%);
}
}
.pagination {
.disabled a {
background-color: fade(@redash-gray, 14%);
}
li {
a {
background-color: fade(@redash-gray, 15%);
&:hover {
background-color: fade(@redash-gray, 25%);
}
}
}
}
.btn-default {
background-color: fade(@redash-gray, 15%);
}
.btn-transparent {
background-color: transparent !important;
}
.btn-default:hover, .btn-default:focus, .btn-default.focus, .btn-default:active, .btn-default.active, .open > .dropdown-toggle.btn-default {
background-color: fade(@redash-gray, 25%);
}
.btn-default:active:hover, .btn-default.active:hover, .open > .dropdown-toggle.btn-default:hover, .btn-default:active:focus, .btn-default.active:focus, .open > .dropdown-toggle.btn-default:focus, .btn-default:active.focus, .btn-default.active.focus, .open > .dropdown-toggle.btn-default.focus {
color: #333;
background-color: fade(@redash-gray, 45%);
}
.label {
border-radius: 2px;
padding: 3px 6px 4px;
font-weight: 500;
font-size: 11px;
}
.label-default {
background: fade(@redash-gray, 85%);
}
.label-tag-unpublished {
background: fade(@redash-gray, 85%);
}
.label-tag-archived {
.label-warning();
}
.label-tag {
background: fade(@redash-gray, 10%);
color: fade(@redash-gray, 75%);
}
.label-tag-unpublished,
.label-tag-archived,
.label-tag {
margin-right: 3px;
display: inline;
margin-top: 2px;
max-width: 24ch;
.text-overflow();
}
.tab-nav > li > a {
text-transform: capitalize;
}
.table > thead > tr > th {
text-transform: none;
}
.dashboard-header {
position: -webkit-sticky; // required for Safari
position: sticky;
background: #f6f7f9;
z-index: 99;
width: 100%;
top: 0;
}
.dashboard__control {
margin: 8px 0;
}
.editing-mode {
a.query-link {
pointer-events: none;
cursor: move;
}
.th-title {
cursor: move;
}
}
.dashboard-header {
position: -webkit-sticky; // required for Safari
position: sticky;
background: #f6f7f9;
z-index: 99;
width: 100%;
top: 0;
}
.widget-wrapper {
.parameter-container {
padding: 0 15px;
}
}
.bg-ace {
background-color: fade(@redash-gray, 12%) !important;
}
.tiled {
border-radius: 3px;
box-shadow: fade(@redash-gray, 15%) 0px 4px 9px -3px;
}
.tile {
border-radius: 3px;
box-shadow: fade(@redash-gray, 15%) 0px 4px 9px -3px;
.widget-menu-regular, .btn__refresh {
opacity: 0 !important;
transition: opacity 0.35s ease-in-out;
}
.t-header {
.th-title {
a {
color: fade(@redash-black, 80%);
font-size: 15px;
font-weight: 500;
}
}
.query--description {
font-size: 14px;
line-height: 1.5;
font-style: italic;
p {
margin-bottom: 0;
}
}
}
.t-header.widget {
padding: 15px;
}
&:hover {
.widget-menu-regular, .btn__refresh {
opacity: 1 !important;
transition: opacity 0.35s ease-in-out;
}
}
.tile__bottom-control {
padding: 10px 15px;
line-height: 2;
}
}
.embed-heading {
h3 {
line-height: 1.75;
margin: 0;
}
}
.filter-container {
margin-bottom: 5px;
}
// Navigation
.caret--nav {
border-top: none;
}
.caret--nav:after {
content: "";
position: absolute;
right: 5px;
top: 9px;
width: 13px;
height: 13px;
display: block;
background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='11px' height='6px' viewBox='0 0 11 6' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3C!-- Generator: Sketch 42 %2836781%29 - http://www.bohemiancoding.com/sketch --%3E%3Ctitle%3EShape%3C/title%3E%3Cdesc%3ECreated with Sketch.%3C/desc%3E%3Cdefs%3E%3C/defs%3E%3Cg id='Page-1' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cpath d='M5.296,4.288 L9.382,0.2 C9.66086822,-0.0716916976 10.1065187,-0.068122925 10.381,0.208 C10.661,0.488 10.661,0.932 10.388,1.206 L5.792,5.803 C5.6602899,5.93388911 5.48167943,6.00662966 5.296,6.005 C5.10997499,6.00689786 4.93095449,5.93413702 4.799,5.803 L0.204,1.207 C0.072163111,1.07394937 -0.00121750401,0.893846387 9.62313189e-05,0.706545264 C0.00140996665,0.519244142 0.0773097323,0.340188219 0.211,0.209 C0.485365732,-0.0664648737 0.930253538,-0.0700311086 1.209,0.201 L5.296,4.288 L5.296,4.288 Z' id='Shape' fill='%23000000'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
background-size: 100% 100%;
transition: transform .2s cubic-bezier(.75,0,.25,1);
}
.navbar .caret--nav:after {
top: 19px;
}
.dropdown--profile .caret--nav:after {
right: 8px;
}
.btn--create {
padding-right: 20px;
.caret--nav:after {
top: 10px;
right: 10px;
background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='11px' height='6px' viewBox='0 0 11 6' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3C!-- Generator: Sketch 42 %2836781%29 - http://www.bohemiancoding.com/sketch --%3E%3Ctitle%3EShape%3C/title%3E%3Cdesc%3ECreated with Sketch.%3C/desc%3E%3Cdefs%3E%3C/defs%3E%3Cg id='Page-1' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cpath d='M5.29592111,4.28945339 L9.38192111,0.201453387 C9.66078932,-0.0702383105 10.1064398,-0.0666695379 10.3809211,0.209453387 C10.6609211,0.489453387 10.6609211,0.933453387 10.3879211,1.20745339 L5.79192111,5.80445339 C5.66021101,5.9353425 5.48160054,6.00808305 5.29592111,6.00645339 C5.1098961,6.00835125 4.9308756,5.9355904 4.79892111,5.80445339 L0.203921109,1.20845339 C0.0720842204,1.07540275 -0.00129639464,0.895299774 1.73406884e-05,0.707998651 C0.00133107602,0.520697529 0.0772308417,0.341641606 0.210921109,0.210453387 C0.485286842,-0.0650114866 0.930174648,-0.0685777215 1.20892111,0.202453387 L5.29592111,4.28945339 L5.29592111,4.28945339 Z' id='Shape' fill='%23FCFCFC'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
}
}
.dropdown.open .caret--nav:after {
transform: rotate(180deg);
}
.collapsing, .collapse.in {
padding: 0;
}
.navbar {
min-height: initial;
height: 50px;
border: 1px solid #fff;
border-top: none;
border-radius: 0;
background: #fff;
margin-bottom: 10px;
.btn-group.open .dropdown-toggle {
-webkit-box-shadow: none;
box-shadow: none;
}
.btn-group .btn:active {
box-shadow: none;
}
}
.navbar-link-ANGULAR_REMOVE_ME {
line-height: 18px;
padding: 10px 15px;
display: block;
@media (min-width: 768px) {
padding-top: 16px;
padding-bottom: 16px;
}
}
.navbar-link-ANGULAR_REMOVE_ME,
.navbar-default .navbar-nav > li > a {
color: #000;
font-weight: 500;
&:active, &:hover, &:focus {
color: #000;
}
}
.navbar-default .btn__new button {
font-weight: 500;
}
.navbar-default .navbar-nav > li > a:hover {
//background-color: fade(@redash-gray, 10%);
//text-decoration: underline;
//border-radius: 0;
}
.navbar-default .navbar-nav > .open > a, .navbar-default .navbar-nav > .open > a:hover, .navbar-default .navbar-nav > .open > a:focus {
background-color: fade(@redash-gray, 15%);
color: #111;
}
.dropdown-menu > li > a:hover, .dropdown-menu > li > a:focus {
background-color: fade(@redash-gray, 15%);
color: #111;
}
.tab-nav {
margin-bottom: 0px;
}
.profile__image--navbar {
border-radius: 100%;
margin-right: 3px;
margin-top: -2px;
}
.profile__image--settings {
border-radius: 100%;
}
.profile__image_thumb {
border-radius: 100%;
margin-right: 3px;
margin-top: -2px;
width: 20px;
height: 20px;
}
.user_list__user--invitation-pending {
color: fade(@alert-danger-bg, 75%);
font-weight: 500;
}
.btn__new {
margin-left: 15px;
}
.navbar-btn {
margin-top: 10px;
margin-bottom: 9px;
}
.navbar-brand {
position: absolute;
left: 50%;
margin-left: -25px !important; // center
display: block;
zoom: 0.9;
}
.va-top {
vertical-align: top;
}
.navbar {
box-shadow: fade(@redash-gray, 15%) 0px 4px 9px -3px;
.navbar-collapse {
padding-left: 0;
}
a.dropdown--profile {
padding-top: 10px;
padding-bottom: 10px;
line-height: 2.35;
}
.navbar-inverse {
background-color: @redash-gray;
border: none;
}
}
.menu-search {
margin-top: 2px;
}
.tags-list {
.badge-light {
background: fade(@redash-gray, 10%);
color: fade(@redash-gray, 75%);
}
}
.dropdown-menu--profile {
li {
width: 200px;
}
}
.navbar .collapse.in {
background: #fff;
position: relative;
z-index: 999;
padding: 0 10px 0 10px;
}
// Pagination
.pagination > li > a, .pagination > li > span {
border-radius: 3px !important;
width: 33px;
height: 33px;
line-height: 31px;
}
// Error state
.error-state {
display: flex;
flex-direction: column;
justify-content: flex-start;
text-align: center;
margin-top: 25vh;
padding: 35px;
font-size: 14px;
line-height: 21px;
.error-state__icon {
.zmdi {
font-size: 64px;
color: @redash-gray;
}
}
@media (max-width: 767px) {
margin-top: 10vh;
}
}
// Forms
.form-control {
border-radius: @redash-input-radius;
&:focus {
box-shadow: none !important;
border-color: @blue;
}
&:hover {
border-color: @blue;
}
}
// Plotly
text.slicetext {
text-shadow: 1px 1px 5px #333;
}
// Responsive fixes
@media (max-width: 767px) {
.text-center-xs {
text-align: center !important;
}
.query-page-wrapper {
h3 {
font-size: 18px;
}
favorites-control {
margin-top: -3px;
}
}
.navbar-brand {
left: 2%;
margin-left: 0 !important;
}
//Fix navbar collapse
.navbar .collapse.in {
border: none;
.dropdown-menu--profile {
li {
width: auto;
}
}
.dropdown--profile {
.caret--nav:after {
right: initial !important;
}
}
.dropdown--profile__username {
display: inline-block;
}
.nav__main li a {
padding: 10px 15px;
display: block;
text-align: left;
float: none !important;
}
.navbar-form {
margin-bottom: 0;
margin-top: 0;
}
.navbar-right {
margin-bottom: 0;
}
}
}
@media (min-width: 768px) {
@media (max-width: 880px) {
.navbar-link-ANGULAR_REMOVE_ME,
.navbar-default .navbar-nav > li > a,
.navbar-form {
padding-left: 10px !important;
padding-right: 10px !important;
}
a.navbar-brand {
margin-left: -15px !important;
}
}
@media (max-width: 810px) {
.menu-search {
width: 175px;
}
a.navbar-brand {
margin-left: 13px !important;
}
}
}
@media (max-width: 1084px) {
.dropdown--profile__username {
display: none;
}
}
// Cross-browser fixes
// Firefox
@-moz-document url-prefix() {
.caret--nav::after {
height: 7px;
}
.navbar .caret--nav::after {
top: 22px;
}
.navbar .btn--create .caret--nav::after {
top: 12px;
}
}
// IE10+
@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
.caret--nav::after {
height: 7px;
}
.navbar .caret--nav::after {
top: 22px;
}
.navbar .btn--create .caret--nav::after {
top: 12px;
}
}
.ui-select-choices-row.disabled > span {
background-color: inherit !important;
}
.list-group-item.inactive,
.ui-select-choices-row.disabled {
background-color: #eee !important;
border-color: transparent;
opacity: 0.5;
box-shadow: none;
color: #333;
pointer-events: none;
cursor: not-allowed;
}
.select-option-divider {
margin: 10px 0 !important;
}
.table-data .label-tag {
display: inline-block;
max-width: 135px;
}

View File

@@ -19,8 +19,6 @@
@import 'inc/ie-warning'; @import 'inc/ie-warning';
@import 'inc/flex'; @import 'inc/flex';
@import 'redash/redash-newstyle';
html, body { html, body {
height: 100%; height: 100%;
margin: 0; margin: 0;

View File

@@ -0,0 +1,22 @@
import React, { forwardRef } from 'react';
import AceEditor from 'react-ace';
import './AceEditorInput.less';
function AceEditorInput(props, ref) {
return (
<div className="ace-editor-input">
<AceEditor
ref={ref}
mode="sql"
theme="textmate"
height="100px"
editorProps={{ $blockScrolling: Infinity }}
showPrintMargin={false}
{...props}
/>
</div>
);
}
export default forwardRef(AceEditorInput);

View File

@@ -0,0 +1,11 @@
.ace-editor-input {
// hide ghost cursor when not focused
.ace_hidden-cursors {
opacity: 0;
}
// allow Ant Form feedback icon to hover scrollbar
.ace_scrollbar {
z-index: auto;
}
}

View File

@@ -0,0 +1,83 @@
import React, { useState } from 'react';
import { react2angular } from 'react2angular';
import Card from 'antd/lib/card';
import Button from 'antd/lib/button';
import Typography from 'antd/lib/typography';
import { clientConfig } from '@/services/auth';
import { HelpTrigger } from '@/components/HelpTrigger';
import DynamicComponent from '@/components/DynamicComponent';
import OrgSettings from '@/services/organizationSettings';
const Text = Typography.Text;
export function BeaconConsent() {
const [hide, setHide] = useState(false);
if (!clientConfig.showBeaconConsentMessage || hide) {
return null;
}
const hideConsentCard = () => {
clientConfig.showBeaconConsentMessage = false;
setHide(true);
};
const confirmConsent = (confirm) => {
let message = '🙏 Thank you.';
if (!confirm) {
message = 'Settings Saved.';
}
OrgSettings.save({ beacon_consent: confirm }, message)
// .then(() => {
// // const settings = get(response, 'settings');
// // this.setState({ settings, formValues: { ...settings } });
// })
.finally(hideConsentCard);
};
return (
<DynamicComponent name="BeaconConsent">
<div className="m-t-10 tiled">
<Card
title={(
<>
Would you be ok with sharing anonymous usage data with the Redash team?{' '}
<HelpTrigger type="USAGE_DATA_SHARING" />
</>
)}
bordered={false}
>
<Text>Help Redash improve by automatically sending anonymous usage data:</Text>
<div className="m-t-5">
<ul>
<li> Number of users, queries, dashboards, alerts, widgets and visualizations.</li>
<li> Types of data sources, alert destinations and visualizations.</li>
</ul>
</div>
<Text>All data is aggregated and will never include any sensitive or private data.</Text>
<div className="m-t-5">
<Button type="primary" className="m-r-5" onClick={() => confirmConsent(true)}>
Yes
</Button>
<Button type="default" onClick={() => confirmConsent(false)}>
No
</Button>
</div>
<div className="m-t-15">
<Text type="secondary">
You can change this setting anytime from the <a href="settings/organization">Organization Settings</a> page.
</Text>
</div>
</Card>
</div>
</DynamicComponent>
);
}
export default function init(ngModule) {
ngModule.component('beaconConsent', react2angular(BeaconConsent));
}
init.init = true;

View File

@@ -0,0 +1,80 @@
import React from 'react';
import PropTypes from 'prop-types';
import Button from 'antd/lib/button';
import Tooltip from 'antd/lib/tooltip';
import './CodeBlock.less';
export default class CodeBlock extends React.Component {
static propTypes = {
copyable: PropTypes.bool,
children: PropTypes.node,
};
static defaultProps = {
copyable: false,
children: null,
};
state = { copied: null };
constructor(props) {
super(props);
this.ref = React.createRef();
this.copyFeatureEnabled = props.copyable && document.queryCommandSupported('copy');
this.resetCopyState = null;
}
componentWillUnmount() {
if (this.resetCopyState) {
clearTimeout(this.resetCopyState);
}
}
copy = () => {
// select text
window.getSelection().selectAllChildren(this.ref.current);
// copy
try {
const success = document.execCommand('copy');
if (!success) {
throw new Error();
}
this.setState({ copied: 'Copied!' });
} catch (err) {
this.setState({
copied: 'Copy failed',
});
}
// reset selection
window.getSelection().removeAllRanges();
// reset tooltip
this.resetCopyState = setTimeout(() => this.setState({ copied: null }), 2000);
};
render() {
const { copyable, children, ...props } = this.props;
const copyButton = (
<Tooltip title={this.state.copied || 'Copy'}>
<Button
icon="copy"
type="dashed"
size="small"
onClick={this.copy}
/>
</Tooltip>
);
return (
<div className="code-block">
<code {...props} ref={this.ref}>
{children}
</code>
{this.copyFeatureEnabled && copyButton}
</div>
);
}
}

View File

@@ -0,0 +1,23 @@
@import '~antd/lib/button/style/index';
.code-block {
background: rgba(0, 0, 0, 0.06);
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 2px;
padding: 3px 27px 3px 3px;
position: relative;
min-height: 32px;
code {
padding: 0;
font-size: 85%;
}
.@{btn-prefix-cls} {
position: absolute;
right: 3px;
bottom: 3px;
padding-left: 3px !important;
padding-right: 3px !important;
}
}

View File

@@ -0,0 +1,24 @@
import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import AntCollapse from 'antd/lib/collapse';
export default function Collapse({ collapsed, children, className, ...props }) {
return (
<AntCollapse {...props} activeKey={collapsed ? null : 'content'} className={cx(className, 'ant-collapse-headerless')}>
<AntCollapse.Panel key="content" header="">{children}</AntCollapse.Panel>
</AntCollapse>
);
}
Collapse.propTypes = {
collapsed: PropTypes.bool,
children: PropTypes.node,
className: PropTypes.string,
};
Collapse.defaultProps = {
collapsed: true,
children: null,
className: '',
};

View File

@@ -0,0 +1,12 @@
// ANGULAR_REMOVE_ME
import { react2angular } from 'react2angular';
import ColorPicker from '@/components/ColorPicker';
import './color-box.less';
export default function init(ngModule) {
ngModule.component('colorBox', react2angular(ColorPicker.Swatch));
}
init.init = true;

View File

@@ -0,0 +1,93 @@
import { isNil, isArray, chunk, map, filter, toPairs } from 'lodash';
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import tinycolor from 'tinycolor2';
import TextInput from 'antd/lib/input';
import Typography from 'antd/lib/typography';
import Swatch from './Swatch';
import './input.less';
function preparePresets(presetColors, presetColumns) {
presetColors = isArray(presetColors) ? map(presetColors, v => [null, v]) : toPairs(presetColors);
presetColors = map(presetColors, ([title, value]) => {
if (isNil(value)) {
return [title, null];
}
value = tinycolor(value);
if (value.isValid()) {
return [title, '#' + value.toHex().toUpperCase()];
}
return null;
});
return chunk(filter(presetColors), presetColumns);
}
function validateColor(value, callback, prefix = '#') {
if (isNil(value)) {
callback(null);
}
value = tinycolor(value);
if (value.isValid()) {
callback(prefix + value.toHex().toUpperCase());
}
}
export default function Input({ color, presetColors, presetColumns, onChange, onPressEnter }) {
const [inputValue, setInputValue] = useState('');
const [isInputFocused, setIsInputFocused] = useState(false);
const presets = preparePresets(presetColors, presetColumns);
function handleInputChange(value) {
setInputValue(value);
validateColor(value, onChange);
}
useEffect(() => {
if (!isInputFocused) {
validateColor(color, setInputValue, '');
}
}, [color, isInputFocused]);
return (
<React.Fragment>
{map(presets, (group, index) => (
<div className="color-picker-input-swatches" key={`preset-row-${index}`}>
{map(group, ([title, value]) => (
<Swatch key={value} color={value} title={title} size={30} onClick={() => validateColor(value, onChange)} />
))}
</div>
))}
<div className="color-picker-input">
<TextInput
addonBefore={<Typography.Text type="secondary">#</Typography.Text>}
value={inputValue}
onChange={e => handleInputChange(e.target.value)}
onFocus={() => setIsInputFocused(true)}
onBlur={() => setIsInputFocused(false)}
onPressEnter={onPressEnter}
/>
</div>
</React.Fragment>
);
}
Input.propTypes = {
color: PropTypes.string,
presetColors: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.string), // array of colors (no tooltips)
PropTypes.objectOf(PropTypes.string), // color name => color value
]),
presetColumns: PropTypes.number,
onChange: PropTypes.func,
onPressEnter: PropTypes.func,
};
Input.defaultProps = {
color: '#FFFFFF',
presetColors: null,
presetColumns: 8,
onChange: () => {},
onPressEnter: () => {},
};

View File

@@ -0,0 +1,37 @@
import { isString } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import Tooltip from 'antd/lib/tooltip';
import './swatch.less';
export default function Swatch({ className, color, title, size, ...props }) {
const result = (
<span
className={`color-swatch ${className}`}
style={{ backgroundColor: color, width: size }}
{...props}
/>
);
if (isString(title) && (title !== '')) {
return (
<Tooltip title={title} mouseEnterDelay={0} mouseLeaveDelay={0}>{result}</Tooltip>
);
}
return result;
}
Swatch.propTypes = {
className: PropTypes.string,
title: PropTypes.string,
color: PropTypes.string,
size: PropTypes.number,
};
Swatch.defaultProps = {
className: '',
title: null,
color: 'transparent',
size: 12,
};

View File

@@ -0,0 +1,128 @@
import { toString } from 'lodash';
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import tinycolor from 'tinycolor2';
import Popover from 'antd/lib/popover';
import Card from 'antd/lib/card';
import Tooltip from 'antd/lib/tooltip';
import Icon from 'antd/lib/icon';
import ColorInput from './Input';
import Swatch from './Swatch';
import './index.less';
function validateColor(value, fallback = null) {
value = tinycolor(value);
return value.isValid() ? '#' + value.toHex().toUpperCase() : fallback;
}
export default function ColorPicker({
color, placement, presetColors, presetColumns, triggerSize, interactive, children, onChange,
}) {
const [visible, setVisible] = useState(false);
const [currentColor, setCurrentColor] = useState('');
function handleApply() {
setVisible(false);
if (!interactive) {
onChange(currentColor);
}
}
function handleCancel() {
setVisible(false);
}
const actions = [];
if (!interactive) {
actions.push((
<Tooltip key="cancel" title="Cancel">
<Icon type="close" onClick={handleCancel} />
</Tooltip>
));
actions.push((
<Tooltip key="apply" title="Apply">
<Icon type="check" onClick={handleApply} />
</Tooltip>
));
}
function handleInputChange(newColor) {
setCurrentColor(newColor);
if (interactive) {
onChange(newColor);
}
}
useEffect(() => {
if (visible) {
setCurrentColor(validateColor(color));
}
}, [color, visible]);
return (
<Popover
overlayClassName={`color-picker ${interactive ? 'color-picker-interactive' : 'color-picker-with-actions'}`}
overlayStyle={{ '--color-picker-selected-color': currentColor }}
content={(
<Card
className="color-picker-panel"
bordered={false}
title={toString(currentColor).toUpperCase()}
headStyle={{
backgroundColor: currentColor,
color: tinycolor(currentColor).isLight() ? '#000000' : '#ffffff',
}}
actions={actions}
>
<ColorInput
color={currentColor}
presetColors={presetColors}
presetColumns={presetColumns}
onChange={handleInputChange}
onPressEnter={handleApply}
/>
</Card>
)}
trigger="click"
placement={placement}
visible={visible}
onVisibleChange={setVisible}
>
{children || (<Swatch className="color-picker-trigger" color={validateColor(color)} size={triggerSize} />)}
</Popover>
);
}
ColorPicker.propTypes = {
color: PropTypes.string,
placement: PropTypes.oneOf([
'top', 'left', 'right', 'bottom',
'topLeft', 'topRight', 'bottomLeft', 'bottomRight',
'leftTop', 'leftBottom', 'rightTop', 'rightBottom',
]),
presetColors: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.string), // array of colors (no tooltips)
PropTypes.objectOf(PropTypes.string), // color name => color value
]),
presetColumns: PropTypes.number,
triggerSize: PropTypes.number,
interactive: PropTypes.bool,
children: PropTypes.node,
onChange: PropTypes.func,
};
ColorPicker.defaultProps = {
color: '#FFFFFF',
placement: 'top',
presetColors: null,
presetColumns: 8,
triggerSize: 30,
interactive: false,
children: null,
onChange: () => {},
};
ColorPicker.Input = ColorInput;
ColorPicker.Swatch = Swatch;

View File

@@ -0,0 +1,40 @@
.color-picker {
&.color-picker-with-actions {
&.ant-popover-placement-top,
&.ant-popover-placement-topLeft,
&.ant-popover-placement-topRight,
&.ant-popover-placement-leftBottom,
&.ant-popover-placement-rightBottom {
> .ant-popover-content > .ant-popover-arrow {
border-color: #fafafa; // same as card actions
}
}
}
&.ant-popover-placement-bottom,
&.ant-popover-placement-bottomLeft,
&.ant-popover-placement-bottomRight,
&.ant-popover-placement-leftTop,
&.ant-popover-placement-rightTop {
> .ant-popover-content > .ant-popover-arrow {
border-color: var(--color-picker-selected-color);
}
}
.ant-popover-inner-content {
padding: 0;
}
.ant-card-head {
text-align: center;
border-bottom-color: rgba(0, 0, 0, 0.1);
}
.ant-card-body {
padding: 10px;
}
}
.color-picker-trigger {
cursor: pointer;
}

View File

@@ -0,0 +1,19 @@
.color-picker-input-swatches {
margin: 0 0 10px 0;
text-align: left;
white-space: nowrap;
.color-swatch {
cursor: pointer;
margin: 0 10px 0 0;
&:last-child {
margin-right: 0;
}
}
}
.color-picker-input {
text-align: left;
white-space: nowrap;
}

View File

@@ -0,0 +1,30 @@
.color-swatch {
display: inline-block;
box-sizing: border-box;
vertical-align: middle;
border-radius: 2px;
overflow: hidden;
width: 12px;
@cell-size: 12px;
@cell-color: rgba(0, 0, 0, 0.1);
background-color: transparent;
background-image:
linear-gradient(45deg, @cell-color 25%, transparent 25%),
linear-gradient(-45deg, @cell-color 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, @cell-color 75%),
linear-gradient(-45deg, transparent 75%, @cell-color 75%);
background-size: @cell-size @cell-size;
background-position: 0 0, 0 @cell-size/2, @cell-size/2 -@cell-size/2, -@cell-size/2 0px;
&:before {
content: "";
display: block;
padding-top: ~"calc(100% - 2px)";
background-color: inherit;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 2px;
overflow: hidden;
}
}

View File

@@ -0,0 +1,191 @@
import React from 'react';
import PropTypes from 'prop-types';
import { isEmpty, toUpper, includes } from 'lodash';
import Button from 'antd/lib/button';
import List from 'antd/lib/list';
import Modal from 'antd/lib/modal';
import Input from 'antd/lib/input';
import Steps from 'antd/lib/steps';
import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper';
import { PreviewCard } from '@/components/PreviewCard';
import EmptyState from '@/components/items-list/components/EmptyState';
import DynamicForm from '@/components/dynamic-form/DynamicForm';
import helper from '@/components/dynamic-form/dynamicFormHelper';
import { HelpTrigger, TYPES as HELP_TRIGGER_TYPES } from '@/components/HelpTrigger';
const { Step } = Steps;
const { Search } = Input;
const StepEnum = {
SELECT_TYPE: 0,
CONFIGURE_IT: 1,
DONE: 2,
};
class CreateSourceDialog extends React.Component {
static propTypes = {
dialog: DialogPropType.isRequired,
types: PropTypes.arrayOf(PropTypes.object),
sourceType: PropTypes.string.isRequired,
imageFolder: PropTypes.string.isRequired,
helpTriggerPrefix: PropTypes.string,
onCreate: PropTypes.func.isRequired,
};
static defaultProps = {
types: [],
helpTriggerPrefix: null,
};
state = {
searchText: '',
selectedType: null,
savingSource: false,
currentStep: StepEnum.SELECT_TYPE,
};
selectType = (selectedType) => {
this.setState({ selectedType, currentStep: StepEnum.CONFIGURE_IT });
};
resetType = () => {
if (this.state.currentStep === StepEnum.CONFIGURE_IT) {
this.setState({ searchText: '', selectedType: null, currentStep: StepEnum.SELECT_TYPE });
}
};
createSource = (values, successCallback, errorCallback) => {
const { selectedType, savingSource } = this.state;
if (!savingSource) {
this.setState({ savingSource: true, currentStep: StepEnum.DONE });
this.props.onCreate(selectedType, values).then((data) => {
successCallback('Saved.');
this.props.dialog.close({ success: true, data });
}).catch((error) => {
this.setState({ savingSource: false, currentStep: StepEnum.CONFIGURE_IT });
errorCallback(error.message);
});
}
};
renderTypeSelector() {
const { types } = this.props;
const { searchText } = this.state;
const filteredTypes = types.filter(type => isEmpty(searchText) ||
includes(type.name.toLowerCase(), searchText.toLowerCase()));
return (
<div className="m-t-10">
<Search
placeholder="Search..."
onChange={e => this.setState({ searchText: e.target.value })}
autoFocus
data-test="SearchSource"
/>
<div className="scrollbox p-5 m-t-10" style={{ minHeight: '30vh', maxHeight: '40vh' }}>
{isEmpty(filteredTypes) ? (<EmptyState className="" />) : (
<List
size="small"
dataSource={filteredTypes}
renderItem={item => this.renderItem(item)}
/>
)}
</div>
</div>
);
}
renderForm() {
const { imageFolder, helpTriggerPrefix } = this.props;
const { selectedType } = this.state;
const fields = helper.getFields(selectedType);
const helpTriggerType = `${helpTriggerPrefix}${toUpper(selectedType.type)}`;
return (
<div className="p-5">
<div className="d-flex justify-content-center align-items-center">
<img
className="p-5"
src={`${imageFolder}/${selectedType.type}.png`}
alt={selectedType.name}
width="48"
/>
<h4 className="m-0">{selectedType.name}</h4>
</div>
<div className="text-right">
{HELP_TRIGGER_TYPES[helpTriggerType] && (
<HelpTrigger className="f-13" type={helpTriggerType}>
Setup Instructions <i className="fa fa-question-circle" />
</HelpTrigger>
)}
</div>
<DynamicForm
id="sourceForm"
fields={fields}
onSubmit={this.createSource}
feedbackIcons
hideSubmitButton
/>
</div>
);
}
renderItem(item) {
const { imageFolder } = this.props;
return (
<List.Item
className="p-l-10 p-r-10 clickable"
onClick={() => this.selectType(item)}
>
<PreviewCard title={item.name} imageUrl={`${imageFolder}/${item.type}.png`} roundedImage={false}>
<i className="fa fa-angle-double-right" />
</PreviewCard>
</List.Item>
);
}
render() {
const { currentStep, savingSource } = this.state;
const { dialog, sourceType } = this.props;
return (
<Modal
{...dialog.props}
title={`Create a New ${sourceType}`}
footer={(currentStep === StepEnum.SELECT_TYPE) ? [
(<Button key="cancel" onClick={() => dialog.dismiss()}>Cancel</Button>),
(<Button key="submit" type="primary" disabled>Create</Button>),
] : [
(<Button key="previous" onClick={this.resetType}>Previous</Button>),
(
<Button
key="submit"
htmlType="submit"
form="sourceForm"
type="primary"
loading={savingSource}
data-test="CreateSourceButton"
>
Create
</Button>
),
]}
>
<div data-test="CreateSourceDialog">
<Steps className="hidden-xs m-b-10" size="small" current={currentStep} progressDot>
{currentStep === StepEnum.CONFIGURE_IT ? (
<Step
title={<a>Type Selection</a>}
className="clickable"
onClick={this.resetType}
/>
) : (<Step title="Type Selection" />)}
<Step title="Configuration" />
<Step title="Done" />
</Steps>
{currentStep === StepEnum.SELECT_TYPE && this.renderTypeSelector()}
{currentStep !== StepEnum.SELECT_TYPE && this.renderForm()}
</div>
</Modal>
);
}
}
export default wrapDialog(CreateSourceDialog);

View File

@@ -1,45 +1,49 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import DatePicker from 'antd/lib/date-picker'; import DatePicker from 'antd/lib/date-picker';
import { clientConfig } from '@/services/auth'; import { clientConfig } from '@/services/auth';
import { Moment } from '@/components/proptypes'; import { Moment } from '@/components/proptypes';
export function DateInput({ const DateInput = React.forwardRef(({
defaultValue,
value, value,
onSelect, onSelect,
className, className,
}) { ...props
}, ref) => {
const format = clientConfig.dateFormat || 'YYYY-MM-DD'; const format = clientConfig.dateFormat || 'YYYY-MM-DD';
const additionalAttributes = {}; const additionalAttributes = {};
if (value && value.isValid()) { if (defaultValue && defaultValue.isValid()) {
additionalAttributes.defaultValue = value; additionalAttributes.defaultValue = defaultValue;
}
if (value === null || (value && value.isValid())) {
additionalAttributes.value = value;
} }
return ( return (
<DatePicker <DatePicker
ref={ref}
className={className} className={className}
{...additionalAttributes} {...additionalAttributes}
format={format} format={format}
placeholder="Select Date" placeholder="Select Date"
onChange={onSelect} onChange={onSelect}
{...props}
/> />
); );
} });
DateInput.propTypes = { DateInput.propTypes = {
defaultValue: Moment,
value: Moment, value: Moment,
onSelect: PropTypes.func, onSelect: PropTypes.func,
className: PropTypes.string, className: PropTypes.string,
}; };
DateInput.defaultProps = { DateInput.defaultProps = {
value: null, defaultValue: null,
value: undefined,
onSelect: () => {}, onSelect: () => {},
className: '', className: '',
}; };
export default function init(ngModule) { export default DateInput;
ngModule.component('dateInput', react2angular(DateInput));
}
init.init = true;

View File

@@ -1,47 +1,51 @@
import { isArray } from 'lodash'; import { isArray } from 'lodash';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import DatePicker from 'antd/lib/date-picker'; import DatePicker from 'antd/lib/date-picker';
import { clientConfig } from '@/services/auth'; import { clientConfig } from '@/services/auth';
import { Moment } from '@/components/proptypes'; import { Moment } from '@/components/proptypes';
const { RangePicker } = DatePicker; const { RangePicker } = DatePicker;
export function DateRangeInput({ const DateRangeInput = React.forwardRef(({
defaultValue,
value, value,
onSelect, onSelect,
className, className,
}) { ...props
}, ref) => {
const format = clientConfig.dateFormat || 'YYYY-MM-DD'; const format = clientConfig.dateFormat || 'YYYY-MM-DD';
const additionalAttributes = {}; const additionalAttributes = {};
if (isArray(value) && value[0].isValid() && value[1].isValid()) { if (isArray(defaultValue) && defaultValue[0].isValid() && defaultValue[1].isValid()) {
additionalAttributes.defaultValue = value; additionalAttributes.defaultValue = defaultValue;
}
if (value === null || (isArray(value) && value[0].isValid() && value[1].isValid())) {
additionalAttributes.value = value;
} }
return ( return (
<RangePicker <RangePicker
ref={ref}
className={className} className={className}
{...additionalAttributes} {...additionalAttributes}
format={format} format={format}
onChange={onSelect} onChange={onSelect}
{...props}
/> />
); );
} });
DateRangeInput.propTypes = { DateRangeInput.propTypes = {
defaultValue: PropTypes.arrayOf(Moment),
value: PropTypes.arrayOf(Moment), value: PropTypes.arrayOf(Moment),
onSelect: PropTypes.func, onSelect: PropTypes.func,
className: PropTypes.string, className: PropTypes.string,
}; };
DateRangeInput.defaultProps = { DateRangeInput.defaultProps = {
value: null, defaultValue: null,
value: undefined,
onSelect: () => {}, onSelect: () => {},
className: '', className: '',
}; };
export default function init(ngModule) { export default DateRangeInput;
ngModule.component('dateRangeInput', react2angular(DateRangeInput));
}
init.init = true;

View File

@@ -1,35 +1,42 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import DatePicker from 'antd/lib/date-picker'; import DatePicker from 'antd/lib/date-picker';
import { clientConfig } from '@/services/auth'; import { clientConfig } from '@/services/auth';
import { Moment } from '@/components/proptypes'; import { Moment } from '@/components/proptypes';
export function DateTimeInput({ const DateTimeInput = React.forwardRef(({
defaultValue,
value, value,
withSeconds, withSeconds,
onSelect, onSelect,
className, className,
}) { ...props
}, ref) => {
const format = (clientConfig.dateFormat || 'YYYY-MM-DD') + const format = (clientConfig.dateFormat || 'YYYY-MM-DD') +
(withSeconds ? ' HH:mm:ss' : ' HH:mm'); (withSeconds ? ' HH:mm:ss' : ' HH:mm');
const additionalAttributes = {}; const additionalAttributes = {};
if (value && value.isValid()) { if (defaultValue && defaultValue.isValid()) {
additionalAttributes.defaultValue = value; additionalAttributes.defaultValue = defaultValue;
}
if (value === null || (value && value.isValid())) {
additionalAttributes.value = value;
} }
return ( return (
<DatePicker <DatePicker
ref={ref}
className={className} className={className}
showTime showTime
{...additionalAttributes} {...additionalAttributes}
format={format} format={format}
placeholder="Select Date and Time" placeholder="Select Date and Time"
onChange={onSelect} onChange={onSelect}
{...props}
/> />
); );
} });
DateTimeInput.propTypes = { DateTimeInput.propTypes = {
defaultValue: Moment,
value: Moment, value: Moment,
withSeconds: PropTypes.bool, withSeconds: PropTypes.bool,
onSelect: PropTypes.func, onSelect: PropTypes.func,
@@ -37,14 +44,11 @@ DateTimeInput.propTypes = {
}; };
DateTimeInput.defaultProps = { DateTimeInput.defaultProps = {
value: null, defaultValue: null,
value: undefined,
withSeconds: false, withSeconds: false,
onSelect: () => {}, onSelect: () => {},
className: '', className: '',
}; };
export default function init(ngModule) { export default DateTimeInput;
ngModule.component('dateTimeInput', react2angular(DateTimeInput));
}
init.init = true;

View File

@@ -1,37 +1,44 @@
import { isArray } from 'lodash'; import { isArray } from 'lodash';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import DatePicker from 'antd/lib/date-picker'; import DatePicker from 'antd/lib/date-picker';
import { clientConfig } from '@/services/auth'; import { clientConfig } from '@/services/auth';
import { Moment } from '@/components/proptypes'; import { Moment } from '@/components/proptypes';
const { RangePicker } = DatePicker; const { RangePicker } = DatePicker;
export function DateTimeRangeInput({ const DateTimeRangeInput = React.forwardRef(({
defaultValue,
value, value,
withSeconds, withSeconds,
onSelect, onSelect,
className, className,
}) { ...props
}, ref) => {
const format = (clientConfig.dateFormat || 'YYYY-MM-DD') + const format = (clientConfig.dateFormat || 'YYYY-MM-DD') +
(withSeconds ? ' HH:mm:ss' : ' HH:mm'); (withSeconds ? ' HH:mm:ss' : ' HH:mm');
const additionalAttributes = {}; const additionalAttributes = {};
if (isArray(value) && value[0].isValid() && value[1].isValid()) { if (isArray(defaultValue) && defaultValue[0].isValid() && defaultValue[1].isValid()) {
additionalAttributes.defaultValue = value; additionalAttributes.defaultValue = defaultValue;
}
if (value === null || (isArray(value) && value[0].isValid() && value[1].isValid())) {
additionalAttributes.value = value;
} }
return ( return (
<RangePicker <RangePicker
ref={ref}
className={className} className={className}
showTime showTime
{...additionalAttributes} {...additionalAttributes}
format={format} format={format}
onChange={onSelect} onChange={onSelect}
{...props}
/> />
); );
} });
DateTimeRangeInput.propTypes = { DateTimeRangeInput.propTypes = {
defaultValue: PropTypes.arrayOf(Moment),
value: PropTypes.arrayOf(Moment), value: PropTypes.arrayOf(Moment),
withSeconds: PropTypes.bool, withSeconds: PropTypes.bool,
onSelect: PropTypes.func, onSelect: PropTypes.func,
@@ -39,14 +46,11 @@ DateTimeRangeInput.propTypes = {
}; };
DateTimeRangeInput.defaultProps = { DateTimeRangeInput.defaultProps = {
value: null, defaultValue: null,
value: undefined,
withSeconds: false, withSeconds: false,
onSelect: () => {}, onSelect: () => {},
className: '', className: '',
}; };
export default function init(ngModule) { export default DateTimeRangeInput;
ngModule.component('dateTimeRangeInput', react2angular(DateTimeRangeInput));
}
init.init = true;

View File

@@ -43,7 +43,7 @@ export default class DynamicComponent extends React.Component {
const { name, children, ...props } = this.props; const { name, children, ...props } = this.props;
const RealComponent = componentsRegistry.get(name); const RealComponent = componentsRegistry.get(name);
if (!RealComponent) { if (!RealComponent) {
return null; return children;
} }
return <RealComponent {...props}>{children}</RealComponent>; return <RealComponent {...props}>{children}</RealComponent>;
} }

View File

@@ -1,10 +1,10 @@
import { includes, startsWith, words, capitalize, clone, isNull } from 'lodash'; import { includes, words, capitalize, clone, isNull } from 'lodash';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Checkbox from 'antd/lib/checkbox';
import Modal from 'antd/lib/modal'; import Modal from 'antd/lib/modal';
import Form from 'antd/lib/form'; import Form from 'antd/lib/form';
import Checkbox from 'antd/lib/checkbox';
import Button from 'antd/lib/button'; import Button from 'antd/lib/button';
import Select from 'antd/lib/select'; import Select from 'antd/lib/select';
import Input from 'antd/lib/input'; import Input from 'antd/lib/input';
@@ -20,14 +20,17 @@ function getDefaultTitle(text) {
return capitalize(words(text).join(' ')); // humanize return capitalize(words(text).join(' ')); // humanize
} }
function isTypeDate(type) {
return startsWith(type, 'date') && !isTypeDateRange(type);
}
function isTypeDateRange(type) { function isTypeDateRange(type) {
return /-range/.test(type); return /-range/.test(type);
} }
function joinExampleList(multiValuesOptions) {
const { prefix, suffix } = multiValuesOptions;
return ['value1', 'value2', 'value3']
.map(value => `${prefix}${value}${suffix}`)
.join(',');
}
function NameInput({ name, type, onChange, existingNames, setValidation }) { function NameInput({ name, type, onChange, existingNames, setValidation }) {
let helpText = ''; let helpText = '';
let validateStatus = ''; let validateStatus = '';
@@ -131,7 +134,7 @@ function EditParameterSettingsDialog(props) {
footer={[( footer={[(
<Button key="cancel" onClick={props.dialog.dismiss}>Cancel</Button> <Button key="cancel" onClick={props.dialog.dismiss}>Cancel</Button>
), ( ), (
<Button key="submit" htmlType="submit" disabled={!isFulfilled()} type="primary" form="paramForm"> <Button key="submit" htmlType="submit" disabled={!isFulfilled()} type="primary" form="paramForm" data-test="SaveParameterSettings">
{isNew ? 'Add Parameter' : 'OK'} {isNew ? 'Add Parameter' : 'OK'}
</Button> </Button>
)]} )]}
@@ -150,40 +153,31 @@ function EditParameterSettingsDialog(props) {
<Input <Input
value={isNull(param.title) ? getDefaultTitle(param.name) : param.title} value={isNull(param.title) ? getDefaultTitle(param.name) : param.title}
onChange={e => setParam({ ...param, title: e.target.value })} onChange={e => setParam({ ...param, title: e.target.value })}
data-test="ParameterTitleInput"
/> />
</Form.Item> </Form.Item>
<Form.Item label="Type" {...formItemProps}> <Form.Item label="Type" {...formItemProps}>
<Select value={param.type} onChange={type => setParam({ ...param, type })}> <Select value={param.type} onChange={type => setParam({ ...param, type })} data-test="ParameterTypeSelect">
<Option value="text">Text</Option> <Option value="text" data-test="TextParameterTypeOption">Text</Option>
<Option value="number">Number</Option> <Option value="number" data-test="NumberParameterTypeOption">Number</Option>
<Option value="enum">Dropdown List</Option> <Option value="enum">Dropdown List</Option>
<Option value="query">Query Based Dropdown List</Option> <Option value="query">Query Based Dropdown List</Option>
<Option disabled key="dv1"> <Option disabled key="dv1">
<Divider className="select-option-divider" /> <Divider className="select-option-divider" />
</Option> </Option>
<Option value="date">Date</Option> <Option value="date" data-test="DateParameterTypeOption">Date</Option>
<Option value="datetime-local">Date and Time</Option> <Option value="datetime-local" data-test="DateTimeParameterTypeOption">Date and Time</Option>
<Option value="datetime-with-seconds">Date and Time (with seconds)</Option> <Option value="datetime-with-seconds">Date and Time (with seconds)</Option>
<Option disabled key="dv2"> <Option disabled key="dv2">
<Divider className="select-option-divider" /> <Divider className="select-option-divider" />
</Option> </Option>
<Option value="date-range">Date Range</Option> <Option value="date-range" data-test="DateRangeParameterTypeOption">Date Range</Option>
<Option value="datetime-range">Date and Time Range</Option> <Option value="datetime-range">Date and Time Range</Option>
<Option value="datetime-range-with-seconds">Date and Time Range (with seconds)</Option> <Option value="datetime-range-with-seconds">Date and Time Range (with seconds)</Option>
</Select> </Select>
</Form.Item> </Form.Item>
{isTypeDate(param.type) && (
<Form.Item label=" " colon={false} {...formItemProps}>
<Checkbox
defaultChecked={param.useCurrentDateTime}
onChange={e => setParam({ ...param, useCurrentDateTime: e.target.checked })}
>
Default to Today/Now if no other value is set
</Checkbox>
</Form.Item>
)}
{param.type === 'enum' && ( {param.type === 'enum' && (
<Form.Item label="Values" help="Dropdown list values (newline delimeted)" {...formItemProps}> <Form.Item label="Values" help="Dropdown list values (newline delimited)" {...formItemProps}>
<Input.TextArea <Input.TextArea
rows={3} rows={3}
value={param.enumOptions} value={param.enumOptions}
@@ -200,6 +194,48 @@ function EditParameterSettingsDialog(props) {
/> />
</Form.Item> </Form.Item>
)} )}
{(param.type === 'enum' || param.type === 'query') && (
<Form.Item className="m-b-0" label=" " colon={false} {...formItemProps}>
<Checkbox
defaultChecked={!!param.multiValuesOptions}
onChange={e => setParam({ ...param,
multiValuesOptions: e.target.checked ? {
prefix: '',
suffix: '',
separator: ',',
} : null })}
data-test="AllowMultipleValuesCheckbox"
>
Allow multiple values
</Checkbox>
</Form.Item>
)}
{(param.type === 'enum' || param.type === 'query') && param.multiValuesOptions && (
<Form.Item
label="Quotation"
help={(
<React.Fragment>
Placed in query as: <code>{joinExampleList(param.multiValuesOptions)}</code>
</React.Fragment>
)}
{...formItemProps}
>
<Select
value={param.multiValuesOptions.prefix}
onChange={quoteOption => setParam({ ...param,
multiValuesOptions: {
...param.multiValuesOptions,
prefix: quoteOption,
suffix: quoteOption,
} })}
data-test="QuotationSelect"
>
<Option value="">None (default)</Option>
<Option value="'">Single Quotation Mark</Option>
<Option value={'"'} data-test="DoubleQuotationMarkOption">Double Quotation Mark</Option>
</Select>
</Form.Item>
)}
</Form> </Form>
</Modal> </Modal>
); );

View File

@@ -0,0 +1,93 @@
import React from 'react';
import PropTypes from 'prop-types';
import Dropdown from 'antd/lib/dropdown';
import Menu from 'antd/lib/menu';
import Button from 'antd/lib/button';
import Icon from 'antd/lib/icon';
import { react2angular } from 'react2angular';
import QueryResultsLink from './QueryResultsLink';
export function QueryControlDropdown(props) {
const menu = (
<Menu>
{!props.query.isNew() && (!props.query.is_draft || !props.query.is_archived) && (
<Menu.Item>
<a target="_self" onClick={() => props.openAddToDashboardForm(props.selectedTab)}>
<Icon type="plus-circle" theme="filled" /> Add to Dashboard
</a>
</Menu.Item>
)}
{!props.query.isNew() && (
<Menu.Item>
<a onClick={() => props.showEmbedDialog(props.query, props.selectedTab)} data-test="ShowEmbedDialogButton">
<Icon type="share-alt" /> Embed Elsewhere
</a>
</Menu.Item>
)}
<Menu.Item>
<QueryResultsLink
disabled={props.queryExecuting || !props.queryResult.getData || !props.queryResult.getData()}
query={props.query}
queryResult={props.queryResult}
embed={props.embed}
apiKey={props.apiKey}
>
<Icon type="file" /> Download as CSV File
</QueryResultsLink>
</Menu.Item>
<Menu.Item>
<QueryResultsLink
fileType="xlsx"
disabled={props.queryExecuting || !props.queryResult.getData || !props.queryResult.getData()}
query={props.query}
queryResult={props.queryResult}
embed={props.embed}
apiKey={props.apiKey}
>
<Icon type="file-excel" /> Download as Excel File
</QueryResultsLink>
</Menu.Item>
</Menu>
);
return (
<Dropdown
trigger={['click']}
overlay={menu}
overlayClassName="query-control-dropdown-overlay"
>
<Button data-test="QueryControlDropdownButton">
<Icon type="ellipsis" rotate={90} />
</Button>
</Dropdown>
);
}
QueryControlDropdown.propTypes = {
query: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
queryResult: PropTypes.object, // eslint-disable-line react/forbid-prop-types
queryExecuting: PropTypes.bool.isRequired,
showEmbedDialog: PropTypes.func.isRequired,
embed: PropTypes.bool,
apiKey: PropTypes.string,
selectedTab: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
openAddToDashboardForm: PropTypes.func.isRequired,
};
QueryControlDropdown.defaultProps = {
queryResult: {},
embed: false,
apiKey: '',
selectedTab: '',
};
export default function init(ngModule) {
ngModule.component('queryControlDropdown', react2angular(QueryControlDropdown));
}
init.init = true;

View File

@@ -0,0 +1,47 @@
import React from 'react';
import PropTypes from 'prop-types';
export default function QueryResultsLink(props) {
let href = '';
const { query, queryResult, fileType } = props;
const resultId = queryResult.getId && queryResult.getId();
const resultData = queryResult.getData && queryResult.getData();
if (resultId && resultData && query.name) {
if (query.id) {
href = `api/queries/${query.id}/results/${resultId}.${fileType}${
props.embed ? `?api_key=${props.apiKey}` : ''
}`;
} else {
href = `api/query_results/${resultId}.${fileType}`;
}
}
return (
<a target="_self" disabled={props.disabled} href={href}>
{props.children}
</a>
);
}
QueryResultsLink.propTypes = {
query: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
queryResult: PropTypes.object, // eslint-disable-line react/forbid-prop-types
fileType: PropTypes.string,
disabled: PropTypes.bool.isRequired,
embed: PropTypes.bool,
apiKey: PropTypes.string,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]).isRequired,
};
QueryResultsLink.defaultProps = {
queryResult: {},
fileType: 'csv',
embed: false,
apiKey: '',
};

View File

@@ -0,0 +1,39 @@
import React from 'react';
import PropTypes from 'prop-types';
import Button from 'antd/lib/button';
import Icon from 'antd/lib/icon';
import { react2angular } from 'react2angular';
export function EditVisualizationButton(props) {
return (
<Button
data-test="EditVisualization"
className="edit-visualization"
onClick={() => props.openVisualizationEditor(props.selectedTab)}
>
<Icon type="form" />
<span className="hidden-xs hidden-s hidden-m">
Edit Visualization
</span>
</Button>
);
}
EditVisualizationButton.propTypes = {
openVisualizationEditor: PropTypes.func.isRequired,
selectedTab: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
};
EditVisualizationButton.defaultProps = {
selectedTab: '',
};
export default function init(ngModule) {
ngModule.component('editVisualizationButton', react2angular(EditVisualizationButton));
}
init.init = true;

View File

@@ -37,7 +37,6 @@ export class FavoritesControl extends React.Component {
const title = item.is_favorite ? 'Remove from favorites' : 'Add to favorites'; const title = item.is_favorite ? 'Remove from favorites' : 'Add to favorites';
return ( return (
<a <a
href="javascript:void(0)"
title={title} title={title}
className="btn-favourite" className="btn-favourite"
onClick={event => this.toggleItem(event, item, onChange)} onClick={event => this.toggleItem(event, item, onChange)}

View File

@@ -0,0 +1,155 @@
import { isArray, indexOf, get, map, includes, every, some, toNumber, toLower } from 'lodash';
import moment from 'moment';
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import Select from 'antd/lib/select';
import { formatDateTime, formatDate } from '@/filters/datetime';
const ALL_VALUES = '###Redash::Filters::SelectAll###';
const NONE_VALUES = '###Redash::Filters::Clear###';
export const FilterType = PropTypes.shape({
name: PropTypes.string.isRequired,
friendlyName: PropTypes.string.isRequired,
multiple: PropTypes.bool,
current: PropTypes.oneOfType([
PropTypes.any,
PropTypes.arrayOf(PropTypes.any),
]),
values: PropTypes.arrayOf(PropTypes.any).isRequired,
});
export const FiltersType = PropTypes.arrayOf(FilterType);
function createFilterChangeHandler(filters, onChange) {
return (filter, values) => {
if (isArray(values)) {
values = map(values, value => filter.values[toNumber(value.key)] || value.key);
} else {
const _values = filter.values[toNumber(values.key)];
values = _values !== undefined ? _values : values.key;
}
if (filter.multiple && includes(values, ALL_VALUES)) {
values = [...filter.values];
}
if (filter.multiple && includes(values, NONE_VALUES)) {
values = [];
}
filters = map(filters, f => (f.name === filter.name ? { ...filter, current: values } : f));
onChange(filters);
};
}
export function filterData(rows, filters = []) {
if (!isArray(rows)) {
return [];
}
let result = rows;
if (isArray(filters) && (filters.length > 0)) {
// "every" field's value should match "some" of corresponding filter's values
result = result.filter(row => every(
filters,
(filter) => {
const rowValue = row[filter.name];
const filterValues = isArray(filter.current) ? filter.current : [filter.current];
return some(filterValues, (filterValue) => {
if (moment.isMoment(rowValue)) {
return rowValue.isSame(filterValue);
}
// We compare with either the value or the String representation of the value,
// because Select2 casts true/false to "true"/"false".
return (filterValue === rowValue) || (String(rowValue) === filterValue);
});
},
));
}
return result;
}
function formatValue(value, columnType) {
if (moment.isMoment(value)) {
if (columnType === 'date') {
return formatDate(value);
}
return formatDateTime(value);
}
if (typeof value === 'boolean') {
return value.toString();
}
return value;
}
export function Filters({ filters, onChange }) {
if (filters.length === 0) {
return null;
}
onChange = createFilterChangeHandler(filters, onChange);
return (
<div className="filters-wrapper">
<div className="container bg-white">
<div className="row">
{map(filters, (filter) => {
const options = map(filter.values, (value, index) => (
<Select.Option key={index}>{formatValue(value, get(filter, 'column.type'))}</Select.Option>
));
return (
<div key={filter.name} className="col-sm-6 p-l-0 filter-container">
<label>{filter.friendlyName}</label>
{(options.length === 0) && (
<Select className="w-100" disabled value="No values" />
)}
{(options.length > 0) && (
<Select
labelInValue
className="w-100"
mode={filter.multiple ? 'multiple' : 'default'}
value={isArray(filter.current) ?
map(filter.current,
value => ({ key: `${indexOf(filter.values, value)}`, label: formatValue(value) })) :
({ key: `${indexOf(filter.values, filter.current)}`, label: formatValue(filter.current) })}
allowClear={filter.multiple}
filterOption={(searchText, option) => includes(toLower(option.props.children), toLower(searchText))}
showSearch
onChange={values => onChange(filter, values)}
>
{!filter.multiple && options}
{filter.multiple && [
<Select.Option key={NONE_VALUES}><i className="fa fa-square-o m-r-5" />Clear</Select.Option>,
<Select.Option key={ALL_VALUES}><i className="fa fa-check-square-o m-r-5" />Select All</Select.Option>,
<Select.OptGroup key="Values" title="Values">{options}</Select.OptGroup>,
]}
</Select>
)}
</div>
);
})}
</div>
</div>
</div>
);
}
Filters.propTypes = {
filters: FiltersType.isRequired,
onChange: PropTypes.func, // (name, value) => void
};
Filters.defaultProps = {
onChange: () => {},
};
export default function init(ngModule) {
ngModule.component('filters', react2angular(Filters));
}
init.init = true;

View File

@@ -1,22 +0,0 @@
import React from 'react';
import { react2angular } from 'react2angular';
export function Footer() {
const separator = ' \u2022 ';
return (
<div id="footer">
<a href="https://redash.io">Redash</a>
{separator}
<a href="https://redash.io/help/">Documentation</a>
{separator}
<a href="https://github.com/getredash/redash">Contribute</a>
</div>
);
}
export default function init(ngModule) {
ngModule.component('footer', react2angular(Footer));
}
init.init = true;

View File

@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import cx from 'classnames'; import cx from 'classnames';
import Tooltip from 'antd/lib/tooltip'; import Tooltip from 'antd/lib/tooltip';
import Drawer from 'antd/lib/drawer'; import Drawer from 'antd/lib/drawer';
import Icon from 'antd/lib/icon';
import { BigMessage } from '@/components/BigMessage'; import { BigMessage } from '@/components/BigMessage';
import DynamicComponent from '@/components/DynamicComponent'; import DynamicComponent from '@/components/DynamicComponent';
@@ -12,7 +13,9 @@ import './HelpTrigger.less';
const DOMAIN = 'https://redash.io'; const DOMAIN = 'https://redash.io';
const HELP_PATH = '/help'; const HELP_PATH = '/help';
const IFRAME_TIMEOUT = 20000; const IFRAME_TIMEOUT = 20000;
const TYPES = { const IFRAME_URL_UPDATE_MESSAGE = 'iframe_url';
export const TYPES = {
HOME: [ HOME: [
'', '',
'Help', 'Help',
@@ -25,21 +28,63 @@ const TYPES = {
'/user-guide/dashboards/sharing-dashboards', '/user-guide/dashboards/sharing-dashboards',
'Guide: Sharing and Embedding Dashboards', 'Guide: Sharing and Embedding Dashboards',
], ],
AUTHENTICATION_OPTIONS: [
'/user-guide/users/authentication-options',
'Guide: Authentication Options',
],
USAGE_DATA_SHARING: [
'/open-source/admin-guide/usage-data',
'Help: Anonymous Usage Data Sharing',
],
DS_ATHENA: [
'/data-sources/amazon-athena-setup',
'Guide: Help Setting up Amazon Athena',
],
DS_BIGQUERY: [
'/data-sources/bigquery-setup',
'Guide: Help Setting up BigQuery',
],
DS_URL: [
'/data-sources/querying-urls',
'Guide: Help Setting up URL',
],
DS_MONGODB: [
'/data-sources/mongodb-setup',
'Guide: Help Setting up MongoDB',
],
DS_GOOGLE_SPREADSHEETS: [
'/data-sources/querying-a-google-spreadsheet',
'Guide: Help Setting up Google Spreadsheets',
],
DS_GOOGLE_ANALYTICS: [
'/data-sources/google-analytics-setup',
'Guide: Help Setting up Google Analytics',
],
DS_AXIBASETSD: [
'/data-sources/axibase-time-series-database',
'Guide: Help Setting up Axibase Time Series',
],
DS_RESULTS: [
'/user-guide/querying/query-results-data-source',
'Guide: Help Setting up Query Results',
],
}; };
export class HelpTrigger extends React.Component { export class HelpTrigger extends React.Component {
static propTypes = { static propTypes = {
type: PropTypes.oneOf(Object.keys(TYPES)).isRequired, type: PropTypes.oneOf(Object.keys(TYPES)).isRequired,
className: PropTypes.string, className: PropTypes.string,
} children: PropTypes.node,
};
static defaultProps = { static defaultProps = {
className: null, className: null,
children: <i className="fa fa-question-circle" />,
}; };
iframeRef = null iframeRef = null;
iframeLoadingTimeout = null iframeLoadingTimeout = null;
constructor(props) { constructor(props) {
super(props); super(props);
@@ -50,9 +95,15 @@ export class HelpTrigger extends React.Component {
visible: false, visible: false,
loading: false, loading: false,
error: false, error: false,
currentUrl: null,
}; };
componentDidMount() {
window.addEventListener('message', this.onPostMessageReceived, DOMAIN);
}
componentWillUnmount() { componentWillUnmount() {
window.removeEventListener('message', this.onPostMessageReceived);
clearTimeout(this.iframeLoadingTimeout); clearTimeout(this.iframeLoadingTimeout);
} }
@@ -64,11 +115,20 @@ export class HelpTrigger extends React.Component {
this.iframeLoadingTimeout = setTimeout(() => { this.iframeLoadingTimeout = setTimeout(() => {
this.setState({ error: url, loading: false }); this.setState({ error: url, loading: false });
}, IFRAME_TIMEOUT); // safety }, IFRAME_TIMEOUT); // safety
} };
onIframeLoaded = () => { onIframeLoaded = () => {
this.setState({ loading: false }); this.setState({ loading: false });
clearTimeout(this.iframeLoadingTimeout); clearTimeout(this.iframeLoadingTimeout);
};
onPostMessageReceived = (event) => {
const { type, message: currentUrl } = event.data || {};
if (type !== IFRAME_URL_UPDATE_MESSAGE) {
return;
}
this.setState({ currentUrl });
} }
openDrawer = () => { openDrawer = () => {
@@ -78,25 +138,31 @@ export class HelpTrigger extends React.Component {
// wait for drawer animation to complete so there's no animation jank // wait for drawer animation to complete so there's no animation jank
setTimeout(() => this.loadIframe(url), 300); setTimeout(() => this.loadIframe(url), 300);
} };
closeDrawer = () => { closeDrawer = (event) => {
if (event) {
event.preventDefault();
}
this.setState({ visible: false }); this.setState({ visible: false });
} this.setState({ visible: false, currentUrl: null });
};
render() { render() {
const [, tooltip] = TYPES[this.props.type]; const [, tooltip] = TYPES[this.props.type];
const className = cx('help-trigger', this.props.className); const className = cx('help-trigger', this.props.className);
const url = this.state.currentUrl;
return ( return (
<React.Fragment> <React.Fragment>
<Tooltip title={tooltip}> <Tooltip title={tooltip}>
<a href="javascript: void(0)" onClick={this.openDrawer} className={className}> <a onClick={this.openDrawer} className={className}>
<i className="fa fa-question-circle" /> {this.props.children}
</a> </a>
</Tooltip> </Tooltip>
<Drawer <Drawer
placement="right" placement="right"
closable={false}
onClose={this.closeDrawer} onClose={this.closeDrawer}
visible={this.state.visible} visible={this.state.visible}
className="help-drawer" className="help-drawer"
@@ -104,6 +170,22 @@ export class HelpTrigger extends React.Component {
width={400} width={400}
> >
<div className="drawer-wrapper"> <div className="drawer-wrapper">
<div className="drawer-menu">
{url && (
<Tooltip title="Open page in a new window" placement="left">
{/* eslint-disable-next-line react/jsx-no-target-blank */}
<a href={url} target="_blank">
<i className="fa fa-external-link" />
</a>
</Tooltip>
)}
<Tooltip title="Close" placement="bottom">
<a href="#" onClick={this.closeDrawer}>
<Icon type="close" />
</a>
</Tooltip>
</div>
{/* iframe */} {/* iframe */}
{!this.state.error && ( {!this.state.error && (
<iframe <iframe

View File

@@ -1,5 +1,13 @@
@import '~antd/lib/drawer/style/drawer';
@help-doc-bg: #f7f7f7; // according to https://github.com/getredash/website/blob/13daff2d8b570956565f482236f6245042e8477f/src/scss/_components/_variables.scss#L15
.help-trigger { .help-trigger {
font-size: 15px; font-size: 15px;
&:hover {
cursor: pointer;
}
} }
.help-drawer { .help-drawer {
@@ -20,6 +28,54 @@
justify-content: center; justify-content: center;
} }
.drawer-menu {
position: fixed;
z-index: 1;
top: 13px;
right: 13px;
border-radius: 3px;
background: rgba(@help-doc-bg, .75); // makes it dissolve over help doc bg
border: 2px solid @help-doc-bg;
display: flex;
a {
height: 26px;
width: 26px;
display: flex;
align-items: center;
justify-content: center;
color: @text-color-secondary;
transition: color @animation-duration-slow;
position: relative;
&:hover {
color: @icon-color-hover;
text-decoration: none;
}
.anticon {
font-size: 15px;
}
.fa-external-link {
position: relative;
top: 1px;
font-size: 14px;
}
// divider
&:not(:first-child):before {
content: '';
position: absolute;
width: 1px;
height: 9px;
left: 0;
top: 9px;
border-left: 1px dotted rgba(0,0,0,.12);
}
}
}
iframe { iframe {
width: 0; width: 0;
visibility: hidden; visibility: hidden;

View File

@@ -0,0 +1,20 @@
import React from 'react';
import PropTypes from 'prop-types';
import { $sanitize } from '@/services/ng';
export default function HtmlContent({ children, ...props }) {
return (
<div
{...props}
dangerouslySetInnerHTML={{ __html: $sanitize(children) }} // eslint-disable-line react/no-danger
/>
);
}
HtmlContent.propTypes = {
children: PropTypes.string,
};
HtmlContent.defaultProps = {
children: '',
};

View File

@@ -0,0 +1,32 @@
import React from 'react';
import PropTypes from 'prop-types';
import Button from 'antd/lib/button';
import Badge from 'antd/lib/badge';
import Tooltip from 'antd/lib/tooltip';
import { KeyboardShortcuts } from '@/services/keyboard-shortcuts';
function ParameterApplyButton({ paramCount, onClick }) {
// show spinner when count is empty so the fade out is consistent
const icon = !paramCount ? 'spinner fa-pulse' : 'check';
return (
<div className="parameter-apply-button" data-show={!!paramCount} data-test="ParameterApplyButton">
<Badge count={paramCount}>
<Tooltip title={`${KeyboardShortcuts.modKey} + Enter`}>
<span>
<Button onClick={onClick}>
<i className={`fa fa-${icon}`} /> Apply Changes
</Button>
</span>
</Tooltip>
</Badge>
</div>
);
}
ParameterApplyButton.propTypes = {
onClick: PropTypes.func.isRequired,
paramCount: PropTypes.number.isRequired,
};
export default ParameterApplyButton;

View File

@@ -14,10 +14,9 @@ import Input from 'antd/lib/input';
import Radio from 'antd/lib/radio'; import Radio from 'antd/lib/radio';
import Form from 'antd/lib/form'; import Form from 'antd/lib/form';
import Tooltip from 'antd/lib/tooltip'; import Tooltip from 'antd/lib/tooltip';
import { ParameterValueInput } from '@/components/ParameterValueInput'; import ParameterValueInput from '@/components/ParameterValueInput';
import { ParameterMappingType } from '@/services/widget'; import { ParameterMappingType } from '@/services/widget';
import { clientConfig } from '@/services/auth'; import { Parameter } from '@/services/query';
import { Query, Parameter } from '@/services/query';
import { HelpTrigger } from '@/components/HelpTrigger'; import { HelpTrigger } from '@/components/HelpTrigger';
import './ParameterMappingInput.less'; import './ParameterMappingInput.less';
@@ -120,8 +119,6 @@ export class ParameterMappingInput extends React.Component {
mapping: PropTypes.object, // eslint-disable-line react/forbid-prop-types mapping: PropTypes.object, // eslint-disable-line react/forbid-prop-types
existingParamNames: PropTypes.arrayOf(PropTypes.string), existingParamNames: PropTypes.arrayOf(PropTypes.string),
onChange: PropTypes.func, onChange: PropTypes.func,
clientConfig: PropTypes.any, // eslint-disable-line react/forbid-prop-types
Query: PropTypes.any, // eslint-disable-line react/forbid-prop-types
inputError: PropTypes.string, inputError: PropTypes.string,
}; };
@@ -129,8 +126,6 @@ export class ParameterMappingInput extends React.Component {
mapping: {}, mapping: {},
existingParamNames: [], existingParamNames: [],
onChange: () => {}, onChange: () => {},
clientConfig: null,
Query: null,
inputError: null, inputError: null,
}; };
@@ -159,6 +154,10 @@ export class ParameterMappingInput extends React.Component {
updateParamMapping = (update) => { updateParamMapping = (update) => {
const { onChange, mapping } = this.props; const { onChange, mapping } = this.props;
const newMapping = extend({}, mapping, update); const newMapping = extend({}, mapping, update);
if (newMapping.value !== mapping.value) {
newMapping.param = newMapping.param.clone();
newMapping.param.setValue(newMapping.value);
}
onChange(newMapping); onChange(newMapping);
}; };
@@ -228,9 +227,8 @@ export class ParameterMappingInput extends React.Component {
value={mapping.param.normalizedValue} value={mapping.param.normalizedValue}
enumOptions={mapping.param.enumOptions} enumOptions={mapping.param.enumOptions}
queryId={mapping.param.queryId} queryId={mapping.param.queryId}
parameter={mapping.param}
onSelect={value => this.updateParamMapping({ value })} onSelect={value => this.updateParamMapping({ value })}
clientConfig={this.props.clientConfig}
Query={this.props.Query}
/> />
); );
} }
@@ -345,8 +343,6 @@ class MappingEditor extends React.Component {
mapping={mapping} mapping={mapping}
existingParamNames={this.props.existingParamNames} existingParamNames={this.props.existingParamNames}
onChange={this.onChange} onChange={this.onChange}
clientConfig={clientConfig}
Query={Query}
inputError={inputError} inputError={inputError}
/> />
<footer> <footer>
@@ -540,7 +536,13 @@ export class ParameterMappingListInput extends React.Component {
param = param.clone().setValue(mapping.value); param = param.clone().setValue(mapping.value);
} }
const value = Parameter.getValue(param); let value = Parameter.getValue(param);
// in case of dynamic value display the name instead of value
if (param.hasDynamicValue) {
value = param.dynamicValue.name;
}
return this.getStringValue(value); return this.getStringValue(value);
} }

View File

@@ -1,23 +1,31 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import Select from 'antd/lib/select'; import Select from 'antd/lib/select';
import Input from 'antd/lib/input'; import Input from 'antd/lib/input';
import InputNumber from 'antd/lib/input-number'; import InputNumber from 'antd/lib/input-number';
import { DateInput } from './DateInput'; import DateParameter from '@/components/dynamic-parameters/DateParameter';
import { DateRangeInput } from './DateRangeInput'; import DateRangeParameter from '@/components/dynamic-parameters/DateRangeParameter';
import { DateTimeInput } from './DateTimeInput'; import { toString } from 'lodash';
import { DateTimeRangeInput } from './DateTimeRangeInput';
import { QueryBasedParameterInput } from './QueryBasedParameterInput'; import { QueryBasedParameterInput } from './QueryBasedParameterInput';
import './ParameterValueInput.less';
const { Option } = Select; const { Option } = Select;
export class ParameterValueInput extends React.Component { const multipleValuesProps = {
maxTagCount: 3,
maxTagTextLength: 10,
maxTagPlaceholder: num => `+${num.length} more`,
};
class ParameterValueInput extends React.Component {
static propTypes = { static propTypes = {
type: PropTypes.string, type: PropTypes.string,
value: PropTypes.any, // eslint-disable-line react/forbid-prop-types value: PropTypes.any, // eslint-disable-line react/forbid-prop-types
enumOptions: PropTypes.string, enumOptions: PropTypes.string,
queryId: PropTypes.number, queryId: PropTypes.number,
parameter: PropTypes.any, // eslint-disable-line react/forbid-prop-types
allowMultipleValues: PropTypes.bool,
onSelect: PropTypes.func, onSelect: PropTypes.func,
className: PropTypes.string, className: PropTypes.string,
}; };
@@ -27,89 +35,83 @@ export class ParameterValueInput extends React.Component {
value: null, value: null,
enumOptions: '', enumOptions: '',
queryId: null, queryId: null,
parameter: null,
allowMultipleValues: false,
onSelect: () => {}, onSelect: () => {},
className: '', className: '',
}; };
renderDateTimeWithSecondsInput() { constructor(props) {
const { value, onSelect } = this.props; super(props);
this.state = {
value: props.parameter.hasPendingValue ? props.parameter.pendingValue : props.value,
isDirty: props.parameter.hasPendingValue,
};
}
componentDidUpdate = (prevProps) => {
const { value, parameter } = this.props;
// if value prop updated, reset dirty state
if (prevProps.value !== value || prevProps.parameter !== parameter) {
this.setState({
value: parameter.hasPendingValue ? parameter.pendingValue : value,
isDirty: parameter.hasPendingValue,
});
}
}
onSelect = (value) => {
const isDirty = toString(value) !== toString(this.props.value);
this.setState({ value, isDirty });
this.props.onSelect(value, isDirty);
}
renderDateParameter() {
const { type, parameter } = this.props;
const { value } = this.state;
return ( return (
<DateTimeInput <DateParameter
type={type}
className={this.props.className} className={this.props.className}
value={value} value={value}
onSelect={onSelect} parameter={parameter}
withSeconds onSelect={this.onSelect}
/> />
); );
} }
renderDateTimeInput() { renderDateRangeParameter() {
const { value, onSelect } = this.props; const { type, parameter } = this.props;
const { value } = this.state;
return ( return (
<DateTimeInput <DateRangeParameter
type={type}
className={this.props.className} className={this.props.className}
value={value} value={value}
onSelect={onSelect} parameter={parameter}
/> onSelect={this.onSelect}
);
}
renderDateInput() {
const { value, onSelect } = this.props;
return (
<DateInput
className={this.props.className}
value={value}
onSelect={onSelect}
/>
);
}
renderDateTimeRangeWithSecondsInput() {
const { value, onSelect } = this.props;
return (
<DateTimeRangeInput
className={this.props.className}
value={value}
onSelect={onSelect}
withSeconds
/>
);
}
renderDateTimeRangeInput() {
const { value, onSelect } = this.props;
return (
<DateTimeRangeInput
className={this.props.className}
value={value}
onSelect={onSelect}
/>
);
}
renderDateRangeInput() {
const { value, onSelect } = this.props;
return (
<DateRangeInput
className={this.props.className}
value={value}
onSelect={onSelect}
/> />
); );
} }
renderEnumInput() { renderEnumInput() {
const { value, onSelect, enumOptions } = this.props; const { enumOptions, allowMultipleValues } = this.props;
const { value } = this.state;
const enumOptionsArray = enumOptions.split('\n').filter(v => v !== ''); const enumOptionsArray = enumOptions.split('\n').filter(v => v !== '');
return ( return (
<Select <Select
className={this.props.className} className={this.props.className}
mode={allowMultipleValues ? 'multiple' : 'default'}
optionFilterProp="children"
disabled={enumOptionsArray.length === 0} disabled={enumOptionsArray.length === 0}
defaultValue={value} value={value}
onChange={onSelect} onChange={this.onSelect}
dropdownMatchSelectWidth={false} dropdownMatchSelectWidth={false}
dropdownClassName="ant-dropdown-in-bootstrap-modal" showSearch
showArrow
style={{ minWidth: 60 }}
notFoundContent={null}
{...multipleValuesProps}
> >
{enumOptionsArray.map(option => (<Option key={option} value={option}>{ option }</Option>))} {enumOptionsArray.map(option => (<Option key={option} value={option}>{ option }</Option>))}
</Select> </Select>
@@ -117,78 +119,77 @@ export class ParameterValueInput extends React.Component {
} }
renderQueryBasedInput() { renderQueryBasedInput() {
const { value, onSelect, queryId } = this.props; const { queryId, parameter, allowMultipleValues } = this.props;
const { value } = this.state;
return ( return (
<QueryBasedParameterInput <QueryBasedParameterInput
className={this.props.className} className={this.props.className}
mode={allowMultipleValues ? 'multiple' : 'default'}
optionFilterProp="children"
parameter={parameter}
value={value} value={value}
queryId={queryId} queryId={queryId}
onSelect={onSelect} onSelect={this.onSelect}
style={{ minWidth: 60 }}
{...multipleValuesProps}
/> />
); );
} }
renderNumberInput() { renderNumberInput() {
const { value, onSelect, className } = this.props; const { className } = this.props;
const { value } = this.state;
const normalize = val => (isNaN(val) ? undefined : val);
return ( return (
<InputNumber <InputNumber
className={'form-control ' + className} className={className}
defaultValue={!isNaN(value) && value || 0} value={normalize(value)}
onChange={onSelect} onChange={val => this.onSelect(normalize(val))}
/> />
); );
} }
renderTextInput() { renderTextInput() {
const { value, onSelect, className } = this.props; const { className } = this.props;
const { value } = this.state;
return ( return (
<Input <Input
className={'form-control ' + className} className={className}
defaultValue={value || ''} value={value}
onChange={event => onSelect(event.target.value)} data-test="TextParamInput"
onChange={e => this.onSelect(e.target.value)}
/> />
); );
} }
render() { renderInput() {
const { type } = this.props; const { type } = this.props;
switch (type) { switch (type) {
case 'datetime-with-seconds': return this.renderDateTimeWithSecondsInput(); case 'datetime-with-seconds':
case 'datetime-local': return this.renderDateTimeInput(); case 'datetime-local':
case 'date': return this.renderDateInput(); case 'date': return this.renderDateParameter();
case 'datetime-range-with-seconds': return this.renderDateTimeRangeWithSecondsInput(); case 'datetime-range-with-seconds':
case 'datetime-range': return this.renderDateTimeRangeInput(); case 'datetime-range':
case 'date-range': return this.renderDateRangeInput(); case 'date-range': return this.renderDateRangeParameter();
case 'enum': return this.renderEnumInput(); case 'enum': return this.renderEnumInput();
case 'query': return this.renderQueryBasedInput(); case 'query': return this.renderQueryBasedInput();
case 'number': return this.renderNumberInput(); case 'number': return this.renderNumberInput();
default: return this.renderTextInput(); default: return this.renderTextInput();
} }
} }
render() {
const { isDirty } = this.state;
return (
<div className="parameter-input" data-dirty={isDirty || null}>
{this.renderInput()}
</div>
);
}
} }
export default function init(ngModule) { export default ParameterValueInput;
ngModule.component('parameterValueInput', {
template: `
<parameter-value-input-impl
type="$ctrl.param.type"
value="$ctrl.param.normalizedValue"
enum-options="$ctrl.param.enumOptions"
query-id="$ctrl.param.queryId"
on-select="$ctrl.setValue"
></parameter-value-input-impl>
`,
bindings: {
param: '<',
},
controller($scope) {
this.setValue = (value) => {
this.param.setValue(value);
$scope.$applyAsync();
};
},
});
ngModule.component('parameterValueInputImpl', react2angular(ParameterValueInput));
}
init.init = true;

View File

@@ -0,0 +1,26 @@
@import '~antd/lib/input-number/style/index'; // for ant @vars
@input-dirty: #fffce1;
.parameter-input {
display: inline-block;
position: relative;
width: 100%;
.@{ant-prefix}-input,
.@{ant-prefix}-input-number {
min-width: 100% !important;
}
.@{ant-prefix}-select {
width: 100%;
}
&[data-dirty] {
.@{ant-prefix}-input, // covers also ant date component
.@{ant-prefix}-input-number,
.@{ant-prefix}-select-selection {
background-color: @input-dirty;
}
}
}

View File

@@ -0,0 +1,208 @@
import React from 'react';
import PropTypes from 'prop-types';
import { size, filter, forEach, extend } from 'lodash';
import { react2angular } from 'react2angular';
import { sortableContainer, sortableElement, sortableHandle } from 'react-sortable-hoc';
import { $location } from '@/services/ng';
import { Parameter } from '@/services/query';
import ParameterApplyButton from '@/components/ParameterApplyButton';
import ParameterValueInput from '@/components/ParameterValueInput';
import EditParameterSettingsDialog from './EditParameterSettingsDialog';
import { toHuman } from '@/filters';
import './Parameters.less';
const DragHandle = sortableHandle(({ parameterName }) => (
<div className="drag-handle" data-test={`DragHandle-${parameterName}`} />
));
const SortableItem = sortableElement(({ className, parameterName, disabled, children }) => (
<div className={className} data-editable={!disabled || null}>
{!disabled && <DragHandle parameterName={parameterName} />}
{children}
</div>
));
const SortableContainer = sortableContainer(({ children }) => children);
function updateUrl(parameters) {
const params = extend({}, $location.search());
parameters.forEach((param) => {
extend(params, param.toUrlParams());
});
Object.keys(params).forEach(key => params[key] == null && delete params[key]);
$location.search(params);
}
export class Parameters extends React.Component {
static propTypes = {
parameters: PropTypes.arrayOf(PropTypes.instanceOf(Parameter)),
editable: PropTypes.bool,
disableUrlUpdate: PropTypes.bool,
onValuesChange: PropTypes.func,
onPendingValuesChange: PropTypes.func,
onParametersEdit: PropTypes.func,
};
static defaultProps = {
parameters: [],
editable: false,
disableUrlUpdate: false,
onValuesChange: () => {},
onPendingValuesChange: () => {},
onParametersEdit: () => {},
}
constructor(props) {
super(props);
const { parameters } = props;
this.state = { parameters, dragging: false };
if (!props.disableUrlUpdate) {
updateUrl(parameters);
}
}
componentDidUpdate = (prevProps) => {
const { parameters, disableUrlUpdate } = this.props;
if (prevProps.parameters !== parameters) {
this.setState({ parameters });
if (!disableUrlUpdate) {
updateUrl(parameters);
}
}
};
handleKeyDown = (e) => {
// Cmd/Ctrl/Alt + Enter
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey || e.altKey)) {
e.stopPropagation();
this.applyChanges();
}
};
setPendingValue = (param, value, isDirty) => {
const { onPendingValuesChange } = this.props;
this.setState(({ parameters }) => {
if (isDirty) {
param.setPendingValue(value);
} else {
param.clearPendingValue();
}
onPendingValuesChange();
return { parameters };
});
};
moveParameter = ({ oldIndex, newIndex }) => {
const { onParametersEdit } = this.props;
if (oldIndex !== newIndex) {
this.setState(({ parameters }) => {
parameters.splice(newIndex, 0, parameters.splice(oldIndex, 1)[0]);
onParametersEdit();
return { parameters };
});
}
this.setState({ dragging: false });
};
onBeforeSortStart = () => {
this.setState({ dragging: true });
};
applyChanges = () => {
const { onValuesChange, disableUrlUpdate } = this.props;
this.setState(({ parameters }) => {
const parametersWithPendingValues = parameters.filter(p => p.hasPendingValue);
forEach(parameters, p => p.applyPendingValue());
onValuesChange(parametersWithPendingValues);
if (!disableUrlUpdate) {
updateUrl(parameters);
}
return { parameters };
});
};
showParameterSettings = (parameter, index) => {
const { onParametersEdit } = this.props;
EditParameterSettingsDialog
.showModal({ parameter })
.result.then((updated) => {
this.setState(({ parameters }) => {
const updatedParameter = extend(parameter, updated);
parameters[index] = new Parameter(updatedParameter, updatedParameter.parentQueryId);
onParametersEdit();
return { parameters };
});
});
};
renderParameter(param, index) {
const { editable } = this.props;
return (
<div
key={param.name}
className="di-block"
data-test={`ParameterName-${param.name}`}
>
<div className="parameter-heading">
<label>{param.title || toHuman(param.name)}</label>
{editable && (
<button
className="btn btn-default btn-xs m-l-5"
onClick={() => this.showParameterSettings(param, index)}
data-test={`ParameterSettings-${param.name}`}
type="button"
>
<i className="fa fa-cog" />
</button>
)}
</div>
<ParameterValueInput
type={param.type}
value={param.normalizedValue}
parameter={param}
enumOptions={param.enumOptions}
queryId={param.queryId}
allowMultipleValues={!!param.multiValuesOptions}
onSelect={(value, isDirty) => this.setPendingValue(param, value, isDirty)}
/>
</div>
);
}
render() {
const { parameters, dragging } = this.state;
const { editable } = this.props;
const dirtyParamCount = size(filter(parameters, 'hasPendingValue'));
return (
<SortableContainer
axis="xy"
useDragHandle
lockToContainerEdges
helperClass="parameter-dragged"
updateBeforeSortStart={this.onBeforeSortStart}
onSortEnd={this.moveParameter}
>
<div
className="parameter-container"
onKeyDown={dirtyParamCount ? this.handleKeyDown : null}
data-draggable={editable || null}
data-dragging={dragging || null}
>
{parameters.map((param, index) => (
<SortableItem className="parameter-block" key={param.name} index={index} parameterName={param.name} disabled={!editable}>
{this.renderParameter(param, index)}
</SortableItem>
))}
<ParameterApplyButton onClick={this.applyChanges} paramCount={dirtyParamCount} />
</div>
</SortableContainer>
);
}
}
export default function init(ngModule) {
ngModule.component('parameters', react2angular(Parameters));
}
init.init = true;

View File

@@ -0,0 +1,124 @@
@import '../assets/less/ant';
.drag-handle {
background: linear-gradient(90deg, transparent 0px, white 1px, white 2px)
center,
linear-gradient(transparent 0px, white 1px, white 2px) center, #111111;
background-size: 2px 2px;
display: inline-block;
width: 6px;
height: 36px;
vertical-align: bottom;
margin-right: 5px;
cursor: move;
}
.parameter-block {
display: inline-block;
background: white;
padding: 0 12px 6px 0;
vertical-align: top;
.parameter-container[data-draggable] & {
margin: 4px 0 0 4px;
padding: 3px 6px 6px;
}
&.parameter-dragged {
box-shadow: 0 4px 9px -3px rgba(102, 136, 153, 0.15);
width: auto !important;
}
}
.parameter-heading {
display: flex;
align-items: center;
padding-bottom: 4px;
label {
margin-bottom: 1px;
overflow: hidden;
text-overflow: ellipsis;
min-width: 100%;
max-width: 195px;
white-space: nowrap;
.parameter-block[data-editable] & {
min-width: calc(100% - 27px); // make room for settings button
max-width: 195px - 27px;
}
}
}
.parameter-container {
position: relative;
&[data-draggable] {
padding: 0 4px 4px 0;
transition: background-color 200ms ease-out;
transition-delay: 300ms; // short pause before returning to original bgcolor
}
&[data-dragging] {
transition-delay: 0s;
background-color: #f6f8f9;
}
.parameter-apply-button {
display: none; // default for mobile
// "floating" on desktop
@media (min-width: 768px) {
position: absolute;
bottom: -36px;
left: -15px;
border-radius: 2px;
z-index: 1;
transition: opacity 150ms ease-out;
box-shadow: 0 4px 9px -3px rgba(102, 136, 153, 0.15);
background-color: #ffffff;
padding: 4px;
padding-left: 16px;
opacity: 0;
display: block;
pointer-events: none; // so tooltip doesn't remain after button hides
}
&[data-show="true"] {
opacity: 1;
display: block;
pointer-events: auto;
}
button {
padding: 0 8px 0 6px;
color: #2096f3;
border-color: #50acf6;
// smaller on desktop
@media (min-width: 768px) {
font-size: 12px;
height: 27px;
}
&:hover, &:focus, &:active {
background-color: #eef7fe;
}
i {
margin-right: 3px;
}
}
.ant-badge-count {
min-width: 15px;
height: 15px;
padding: 0 5px;
font-size: 10px;
line-height: 15px;
background: #f77b74;
border-radius: 7px;
box-shadow: 0px 0px 0 1px white, -1px 1px 0 1px #5d6f7d85;
}
}
}

View File

@@ -1,12 +1,19 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames';
// PreviewCard // PreviewCard
export function PreviewCard({ imageUrl, title, body, children, className, ...props }) { export function PreviewCard({ imageUrl, roundedImage, title, body, children, className, ...props }) {
return ( return (
<div {...props} className={className + ' w-100 d-flex align-items-center'}> <div {...props} className={className + ' w-100 d-flex align-items-center'}>
<img src={imageUrl} width="32" height="32" className="profile__image--settings m-r-5" alt="Logo/Avatar" /> <img
src={imageUrl}
width="32"
height="32"
className={classNames({ 'profile__image--settings': roundedImage }, 'm-r-5')}
alt="Logo/Avatar"
/>
<div className="flex-fill"> <div className="flex-fill">
<div>{title}</div> <div>{title}</div>
{body && <div className="text-muted">{body}</div>} {body && <div className="text-muted">{body}</div>}
@@ -20,12 +27,14 @@ PreviewCard.propTypes = {
imageUrl: PropTypes.string.isRequired, imageUrl: PropTypes.string.isRequired,
title: PropTypes.node.isRequired, title: PropTypes.node.isRequired,
body: PropTypes.node, body: PropTypes.node,
roundedImage: PropTypes.bool,
className: PropTypes.string, className: PropTypes.string,
children: PropTypes.node, children: PropTypes.node,
}; };
PreviewCard.defaultProps = { PreviewCard.defaultProps = {
body: null, body: null,
roundedImage: true,
className: '', className: '',
children: null, children: null,
}; };

View File

@@ -1,15 +1,16 @@
import { find, isFunction } from 'lodash'; import { find, isFunction, isArray, isEqual, toString, map, intersection } from 'lodash';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { react2angular } from 'react2angular'; import { react2angular } from 'react2angular';
import Select from 'antd/lib/select'; import Select from 'antd/lib/select';
import { Query } from '@/services/query';
const { Option } = Select; const { Option } = Select;
export class QueryBasedParameterInput extends React.Component { export class QueryBasedParameterInput extends React.Component {
static propTypes = { static propTypes = {
parameter: PropTypes.any, // eslint-disable-line react/forbid-prop-types
value: PropTypes.any, // eslint-disable-line react/forbid-prop-types value: PropTypes.any, // eslint-disable-line react/forbid-prop-types
mode: PropTypes.oneOf(['default', 'multiple']),
queryId: PropTypes.number, queryId: PropTypes.number,
onSelect: PropTypes.func, onSelect: PropTypes.func,
className: PropTypes.string, className: PropTypes.string,
@@ -17,6 +18,8 @@ export class QueryBasedParameterInput extends React.Component {
static defaultProps = { static defaultProps = {
value: null, value: null,
mode: 'default',
parameter: null,
queryId: null, queryId: null,
onSelect: () => {}, onSelect: () => {},
className: '', className: '',
@@ -34,31 +37,39 @@ export class QueryBasedParameterInput extends React.Component {
this._loadOptions(this.props.queryId); this._loadOptions(this.props.queryId);
} }
// eslint-disable-next-line no-unused-vars componentDidUpdate(prevProps) {
componentWillReceiveProps(nextProps) { if (this.props.queryId !== prevProps.queryId) {
if (nextProps.queryId !== this.props.queryId) { this._loadOptions(this.props.queryId);
this._loadOptions(nextProps.queryId, nextProps.value);
} }
} }
_loadOptions(queryId) { async _loadOptions(queryId) {
if (queryId && (queryId !== this.state.queryId)) { if (queryId && (queryId !== this.state.queryId)) {
this.setState({ loading: true }); this.setState({ loading: true });
Query.dropdownOptions({ id: queryId }, (options) => { const options = await this.props.parameter.loadDropdownValues();
if (this.props.queryId === queryId) {
this.setState({ options, loading: false });
// stale queryId check
if (this.props.queryId === queryId) {
this.setState({ options, loading: false });
if (this.props.mode === 'multiple' && isArray(this.props.value)) {
const optionValues = map(options, option => option.value);
const validValues = intersection(this.props.value, optionValues);
if (!isEqual(this.props.value, validValues)) {
this.props.onSelect(validValues);
}
} else {
const found = find(options, option => option.value === this.props.value) !== undefined; const found = find(options, option => option.value === this.props.value) !== undefined;
if (!found && isFunction(this.props.onSelect)) { if (!found && isFunction(this.props.onSelect)) {
this.props.onSelect(options[0].value); this.props.onSelect(options[0].value);
} }
} }
}); }
} }
} }
render() { render() {
const { className, value, onSelect } = this.props; const { className, value, mode, onSelect, ...otherProps } = this.props;
const { loading, options } = this.state; const { loading, options } = this.state;
return ( return (
<span> <span>
@@ -66,10 +77,15 @@ export class QueryBasedParameterInput extends React.Component {
className={className} className={className}
disabled={loading || (options.length === 0)} disabled={loading || (options.length === 0)}
loading={loading} loading={loading}
defaultValue={'' + value} mode={mode}
value={isArray(value) ? value : toString(value)}
onChange={onSelect} onChange={onSelect}
dropdownMatchSelectWidth={false} dropdownMatchSelectWidth={false}
dropdownClassName="ant-dropdown-in-bootstrap-modal" optionFilterProp="children"
showSearch
showArrow
notFoundContent={null}
{...otherProps}
> >
{options.map(option => (<Option value={option.value} key={option.value}>{option.name}</Option>))} {options.map(option => (<Option value={option.value} key={option.value}>{option.name}</Option>))}
</Select> </Select>

View File

@@ -5,7 +5,7 @@ import { react2angular } from 'react2angular';
import AceEditor from 'react-ace'; import AceEditor from 'react-ace';
import ace from 'brace'; import ace from 'brace';
import toastr from 'angular-toastr'; import notification from '@/services/notification';
import 'brace/ext/language_tools'; import 'brace/ext/language_tools';
import 'brace/mode/json'; import 'brace/mode/json';
@@ -54,7 +54,7 @@ class QueryEditor extends React.Component {
isDirty: PropTypes.bool.isRequired, isDirty: PropTypes.bool.isRequired,
isQueryOwner: PropTypes.bool.isRequired, isQueryOwner: PropTypes.bool.isRequired,
updateDataSource: PropTypes.func.isRequired, updateDataSource: PropTypes.func.isRequired,
canExecuteQuery: PropTypes.func.isRequired, canExecuteQuery: PropTypes.bool.isRequired,
executeQuery: PropTypes.func.isRequired, executeQuery: PropTypes.func.isRequired,
queryExecuting: PropTypes.bool.isRequired, queryExecuting: PropTypes.bool.isRequired,
saveQuery: PropTypes.func.isRequired, saveQuery: PropTypes.func.isRequired,
@@ -149,6 +149,7 @@ class QueryEditor extends React.Component {
editor.commands.bindKey({ win: 'Ctrl+P', mac: null }, null); editor.commands.bindKey({ win: 'Ctrl+P', mac: null }, null);
// Lineup only mac // Lineup only mac
editor.commands.bindKey({ win: null, mac: 'Ctrl+P' }, 'golineup'); editor.commands.bindKey({ win: null, mac: 'Ctrl+P' }, 'golineup');
editor.commands.bindKey({ win: 'Ctrl+Shift+F', mac: 'Cmd+Shift+F' }, this.formatQuery);
// Reset Completer in case dot is pressed // Reset Completer in case dot is pressed
editor.commands.on('afterExec', (e) => { editor.commands.on('afterExec', (e) => {
@@ -209,7 +210,7 @@ class QueryEditor extends React.Component {
formatQuery = () => { formatQuery = () => {
Query.format(this.props.dataSource.syntax || 'sql', this.props.queryText) Query.format(this.props.dataSource.syntax || 'sql', this.props.queryText)
.then(this.updateQuery) .then(this.updateQuery)
.catch(error => toastr.error(error)); .catch(error => notification.error(error));
}; };
toggleAutocomplete = (state) => { toggleAutocomplete = (state) => {
@@ -226,7 +227,7 @@ class QueryEditor extends React.Component {
render() { render() {
const modKey = KeyboardShortcuts.modKey; const modKey = KeyboardShortcuts.modKey;
const isExecuteDisabled = this.props.queryExecuting || !this.props.canExecuteQuery(); const isExecuteDisabled = this.props.queryExecuting || !this.props.canExecuteQuery;
return ( return (
<section style={{ height: '100%' }} data-test="QueryEditor"> <section style={{ height: '100%' }} data-test="QueryEditor">
@@ -266,7 +267,7 @@ class QueryEditor extends React.Component {
&#123;&#123;&nbsp;&#125;&#125; &#123;&#123;&nbsp;&#125;&#125;
</button> </button>
</Tooltip> </Tooltip>
<Tooltip placement="top" title="Format Query"> <Tooltip placement="top" title={<>Format Query (<i>{modKey} + Shift + F</i>)</>}>
<button type="button" className="btn btn-default m-r-5" onClick={this.formatQuery}> <button type="button" className="btn btn-default m-r-5" onClick={this.formatQuery}>
<span className="zmdi zmdi-format-indent-increase" /> <span className="zmdi zmdi-format-indent-increase" />
</button> </button>
@@ -289,7 +290,13 @@ class QueryEditor extends React.Component {
</select> </select>
{this.props.canEdit ? ( {this.props.canEdit ? (
<Tooltip placement="top" title={modKey + ' + S'}> <Tooltip placement="top" title={modKey + ' + S'}>
<button type="button" className="btn btn-default m-l-5" onClick={this.props.saveQuery} title="Save"> <button
type="button"
className="btn btn-default m-l-5"
onClick={this.props.saveQuery}
data-test="SaveButton"
title="Save"
>
<span className="fa fa-floppy-o" /> <span className="fa fa-floppy-o" />
<span className="hidden-xs m-l-5">Save</span> <span className="hidden-xs m-l-5">Save</span>
{this.props.isDirty ? '*' : null} {this.props.isDirty ? '*' : null}

View File

@@ -6,7 +6,7 @@ import { debounce, find } from 'lodash';
import Input from 'antd/lib/input'; import Input from 'antd/lib/input';
import Select from 'antd/lib/select'; import Select from 'antd/lib/select';
import { Query } from '@/services/query'; import { Query } from '@/services/query';
import { toastr } from '@/services/ng'; import notification from '@/services/notification';
import { QueryTagsControl } from '@/components/tags-control/TagsControl'; import { QueryTagsControl } from '@/components/tags-control/TagsControl';
const SEARCH_DEBOUNCE_DURATION = 200; const SEARCH_DEBOUNCE_DURATION = 200;
@@ -94,7 +94,7 @@ export function QuerySelector(props) {
if (queryId) { if (queryId) {
query = find(searchResults, { id: queryId }); query = find(searchResults, { id: queryId });
if (!query) { // shouldn't happen if (!query) { // shouldn't happen
toastr.error('Something went wrong... Couldn\'t select query'); notification.error('Something went wrong...', 'Couldn\'t select query');
} }
} }
@@ -112,10 +112,10 @@ export function QuerySelector(props) {
<div className="list-group"> <div className="list-group">
{searchResults.map(q => ( {searchResults.map(q => (
<a <a
href="javascript:void(0)" className={cx('query-selector-result', 'list-group-item', { inactive: q.is_draft })}
className={cx('list-group-item', { inactive: q.is_draft })}
key={q.id} key={q.id}
onClick={() => selectQuery(q.id)} onClick={() => selectQuery(q.id)}
data-test={`QueryId${q.id}`}
> >
{q.name} {q.name}
{' '} {' '}

View File

@@ -9,7 +9,7 @@ import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper';
import { BigMessage } from '@/components/BigMessage'; import { BigMessage } from '@/components/BigMessage';
import LoadingState from '@/components/items-list/components/LoadingState'; import LoadingState from '@/components/items-list/components/LoadingState';
import { toastr } from '@/services/ng'; import notification from '@/services/notification';
class SelectItemsDialog extends React.Component { class SelectItemsDialog extends React.Component {
static propTypes = { static propTypes = {
@@ -100,7 +100,7 @@ class SelectItemsDialog extends React.Component {
}) })
.catch(() => { .catch(() => {
this.setState({ saveInProgress: false }); this.setState({ saveInProgress: false });
toastr.error('Failed to save some of selected items.'); notification.error('Failed to save some of selected items.');
}); });
}); });
} }

View File

@@ -63,7 +63,6 @@ export class TagsList extends React.Component {
{map(allTags, tag => ( {map(allTags, tag => (
<a <a
key={tag.name} key={tag.name}
href="javascript:void(0)"
className={classNames('list-group-item', 'max-character', { active: selectedTags.has(tag.name) })} className={classNames('list-group-item', 'max-character', { active: selectedTags.has(tag.name) })}
onClick={event => this.toggleTag(event, tag.name)} onClick={event => this.toggleTag(event, tag.name)}
> >

View File

@@ -1,83 +1,70 @@
import moment from 'moment'; import moment from 'moment';
import { isNil } from 'lodash'; import { isNil } from 'lodash';
import React from 'react'; import React, { useEffect } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Moment } from '@/components/proptypes'; import { Moment } from '@/components/proptypes';
import { clientConfig } from '@/services/auth'; import { clientConfig } from '@/services/auth';
import useForceUpdate from '@/lib/hooks/useForceUpdate';
import Tooltip from 'antd/lib/tooltip';
const autoUpdateList = new Set(); function toMoment(value) {
value = !isNil(value) ? moment(value) : null;
function updateComponents() { return value && value.isValid() ? value : null;
autoUpdateList.forEach(component => component.update());
setTimeout(updateComponents, 30 * 1000);
} }
updateComponents();
export class TimeAgo extends React.PureComponent { export function TimeAgo({ date, placeholder, autoUpdate }) {
static propTypes = { const startDate = toMoment(date);
// `date` and `placeholder` used in `getDerivedStateFromProps`
// eslint-disable-next-line react/no-unused-prop-types
date: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.instanceOf(Date),
Moment,
]),
// eslint-disable-next-line react/no-unused-prop-types
placeholder: PropTypes.string,
autoUpdate: PropTypes.bool,
};
static defaultProps = { const value = startDate ? startDate.fromNow() : placeholder;
date: null, const title = startDate ? startDate.format(clientConfig.dateTimeFormat) : '';
placeholder: '',
autoUpdate: true,
};
// Initial state, to get rid of React warning const forceUpdate = useForceUpdate();
state = {
title: null,
value: null,
};
static getDerivedStateFromProps({ date, placeholder }) { useEffect(() => {
// if `date` prop is not empty and a valid date/time - convert it to `moment` if (autoUpdate) {
date = !isNil(date) ? moment(date) : null; const timer = setInterval(forceUpdate, 30 * 1000);
date = date && date.isValid() ? date : null; return () => clearInterval(timer);
return {
value: date ? date.fromNow() : placeholder,
title: date ? date.format(clientConfig.dateTimeFormat) : '',
};
}
componentDidMount() {
autoUpdateList.add(this);
this.update(true);
}
componentWillUnmount() {
autoUpdateList.delete(this);
}
update(force = false) {
if (force || this.props.autoUpdate) {
this.setState(this.constructor.getDerivedStateFromProps(this.props));
} }
} }, [autoUpdate]);
render() { return (
return <span title={this.state.title} data-test="TimeAgo">{this.state.value}</span>; <Tooltip title={title}>
} <span data-test="TimeAgo">{value}</span>
</Tooltip>
);
} }
TimeAgo.propTypes = {
// `date` and `placeholder` used in `getDerivedStateFromProps`
// eslint-disable-next-line react/no-unused-prop-types
date: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.instanceOf(Date),
Moment,
]),
// eslint-disable-next-line react/no-unused-prop-types
placeholder: PropTypes.string,
autoUpdate: PropTypes.bool,
};
TimeAgo.defaultProps = {
date: null,
placeholder: '',
autoUpdate: true,
};
export default function init(ngModule) { export default function init(ngModule) {
ngModule.directive('amTimeAgo', () => ({ ngModule.directive('amTimeAgo', () => ({
link($scope, element, attr) { link($scope, $element, attr) {
const modelName = attr.amTimeAgo; const modelName = attr.amTimeAgo;
$scope.$watch(modelName, (value) => { $scope.$watch(modelName, (value) => {
ReactDOM.render(<TimeAgo date={value} />, element[0]); ReactDOM.render(<TimeAgo date={value} />, $element[0]);
});
$scope.$on('$destroy', () => {
ReactDOM.unmountComponentAtNode($element[0]);
}); });
}, },
})); }));
@@ -91,6 +78,10 @@ export default function init(ngModule) {
// Initial render will occur here as well // Initial render will occur here as well
ReactDOM.render(<TimeAgo date={this.value} placeholder="-" />, $element[0]); ReactDOM.render(<TimeAgo date={this.value} placeholder="-" />, $element[0]);
}); });
$scope.$on('$destroy', () => {
ReactDOM.unmountComponentAtNode($element[0]);
});
}, },
}); });
} }

View File

@@ -0,0 +1,40 @@
import moment from 'moment';
import { useMemo, useEffect } from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import { Moment } from '@/components/proptypes';
import useForceUpdate from '@/lib/hooks/useForceUpdate';
export function Timer({ from }) {
const startTime = useMemo(() => moment(from).valueOf(), [from]);
const forceUpdate = useForceUpdate();
useEffect(() => {
const timer = setInterval(forceUpdate, 1000);
return () => clearInterval(timer);
}, []);
const diff = moment.now() - startTime;
const format = diff > 1000 * 60 * 60 ? 'HH:mm:ss' : 'mm:ss'; // no HH under an hour
return moment.utc(diff).format(format);
}
Timer.propTypes = {
from: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.instanceOf(Date),
Moment,
]),
};
Timer.defaultProps = {
from: null,
};
export default function init(ngModule) {
ngModule.component('rdTimer', react2angular(Timer));
}
init.init = true;

View File

@@ -1,60 +1,16 @@
import { map } from 'lodash';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { $http } from '@/services/ng';
import Table from 'antd/lib/table'; import Table from 'antd/lib/table';
import Col from 'antd/lib/col';
import Row from 'antd/lib/row';
import Card from 'antd/lib/card'; import Card from 'antd/lib/card';
import Spin from 'antd/lib/spin'; import Spin from 'antd/lib/spin';
import Badge from 'antd/lib/badge'; import Badge from 'antd/lib/badge';
import Tabs from 'antd/lib/tabs';
import Alert from 'antd/lib/alert';
import moment from 'moment';
import values from 'lodash/values';
import { Columns } from '@/components/items-list/components/ItemsTable'; import { Columns } from '@/components/items-list/components/ItemsTable';
function parseTasks(tasks) { // CounterCard
const queues = {};
const queries = [];
const otherTasks = [];
const counters = { active: 0, reserved: 0, waiting: 0 }; export function CounterCard({ title, value, loading }) {
tasks.forEach((task) => {
queues[task.queue] = queues[task.queue] || { name: task.queue, active: 0, reserved: 0, waiting: 0 };
queues[task.queue][task.state] += 1;
if (task.enqueue_time) {
task.enqueue_time = moment(task.enqueue_time * 1000.0);
}
if (task.start_time) {
task.start_time = moment(task.start_time * 1000.0);
}
counters[task.state] += 1;
if (task.task_name === 'redash.tasks.execute_query') {
queries.push(task);
} else {
otherTasks.push(task);
}
});
return { queues: values(queues), queries, otherTasks, counters };
}
function QueuesTable({ loading, queues }) {
const columns = ['Name', 'Active', 'Reserved', 'Waiting'].map(c => ({ title: c, dataIndex: c.toLowerCase() }));
return <Table columns={columns} rowKey="name" dataSource={queues} loading={loading} />;
}
QueuesTable.propTypes = {
loading: PropTypes.bool.isRequired,
queues: PropTypes.arrayOf(PropTypes.any).isRequired,
};
function CounterCard({ title, value, loading }) {
return ( return (
<Spin spinning={loading}> <Spin spinning={loading}>
<Card> <Card>
@@ -75,145 +31,82 @@ CounterCard.defaultProps = {
value: '', value: '',
}; };
export default class AdminCeleryStatus extends React.Component { // Tables
state = {
loading: true,
error: false,
counters: {},
queries: [],
otherTasks: [],
queues: [],
};
constructor(props) { const commonColumns = [
super(props); { title: 'Worker Name', dataIndex: 'worker' },
this.fetch(); { title: 'PID', dataIndex: 'worker_pid' },
} { title: 'Queue', dataIndex: 'queue' },
Columns.custom((value) => {
fetch() { if (value === 'active') {
// TODO: handle error return <span><Badge status="processing" /> Active</span>;
$http
.get('/api/admin/queries/tasks')
.then(({ data }) => {
const { queues, queries, otherTasks, counters } = parseTasks(data.tasks);
this.setState({ loading: false, queries, otherTasks, queues, counters });
})
.catch(() => {
this.setState({ loading: false, error: true });
});
}
render() {
const commonColumns = [
{
title: 'Worker Name',
dataIndex: 'worker',
},
{
title: 'PID',
dataIndex: 'worker_pid',
},
{
title: 'Queue',
dataIndex: 'queue',
},
{
title: 'State',
dataIndex: 'state',
render: (value) => {
if (value === 'active') {
return (
<span>
<Badge status="processing" /> Active
</span>
);
}
return (
<span>
<Badge status="warning" /> {value}
</span>
);
},
},
Columns.timeAgo({ title: 'Start Time', dataIndex: 'start_time' }),
];
const queryColumns = commonColumns.concat([
Columns.timeAgo({ title: 'Enqueue Time', dataIndex: 'enqueue_time' }),
{
title: 'Query ID',
dataIndex: 'query_id',
},
{
title: 'Org ID',
dataIndex: 'org_id',
},
{
title: 'Data Source ID',
dataIndex: 'data_source_id',
},
{
title: 'User ID',
dataIndex: 'user_id',
},
{
title: 'Scheduled',
dataIndex: 'scheduled',
},
]);
const otherTasksColumns = commonColumns.concat([
{
title: 'Task Name',
dataIndex: 'task_name',
},
]);
if (this.state.error) {
return (
<div className="p-5">
<Alert type="error" message="Failed loading status. Please refresh." />
</div>
);
} }
return <span><Badge status="warning" /> {value}</span>;
}, {
title: 'State',
dataIndex: 'state',
}),
Columns.timeAgo({ title: 'Start Time', dataIndex: 'start_time' }),
];
return ( const queryColumns = commonColumns.concat([
<div className="p-5"> Columns.timeAgo({ title: 'Enqueue Time', dataIndex: 'enqueue_time' }),
<Row gutter={16}> { title: 'Query ID', dataIndex: 'query_id' },
<Col span={4}> { title: 'Org ID', dataIndex: 'org_id' },
<CounterCard title="Active Tasks" value={this.state.counters.active} loading={this.state.loading} /> { title: 'Data Source ID', dataIndex: 'data_source_id' },
</Col> { title: 'User ID', dataIndex: 'user_id' },
<Col span={4}> { title: 'Scheduled', dataIndex: 'scheduled' },
<CounterCard title="Reserved Tasks" value={this.state.counters.reserved} loading={this.state.loading} /> ]);
</Col>
<Col span={4}> const otherTasksColumns = commonColumns.concat([
<CounterCard title="Waiting Tasks" value={this.state.counters.waiting} loading={this.state.loading} /> { title: 'Task Name', dataIndex: 'task_name' },
</Col> ]);
</Row>
<Row> const queuesColumns = map(
<Tabs defaultActiveKey="queues"> ['Name', 'Active', 'Reserved', 'Waiting'],
<Tabs.TabPane key="queues" tab="Queues"> c => ({ title: c, dataIndex: c.toLowerCase() }),
<QueuesTable loading={this.state.loading} queues={this.state.queues} /> );
</Tabs.TabPane>
<Tabs.TabPane key="queries" tab="Queries"> const TablePropTypes = {
<Table loading: PropTypes.bool.isRequired,
rowKey="task_id" items: PropTypes.arrayOf(PropTypes.object).isRequired,
dataSource={this.state.queries} };
loading={this.state.loading}
columns={queryColumns} export function QueuesTable({ loading, items }) {
/> return (
</Tabs.TabPane> <Table
<Tabs.TabPane key="other" tab="Other Tasks"> loading={loading}
<Table columns={queuesColumns}
rowKey="task_id" rowKey="name"
dataSource={this.state.otherTasks} dataSource={items}
loading={this.state.loading} />
columns={otherTasksColumns} );
/>
</Tabs.TabPane>
</Tabs>
</Row>
</div>
);
}
} }
QueuesTable.propTypes = TablePropTypes;
export function QueriesTable({ loading, items }) {
return (
<Table
loading={loading}
columns={queryColumns}
rowKey="task_id"
dataSource={items}
/>
);
}
QueriesTable.propTypes = TablePropTypes;
export function OtherTasksTable({ loading, items }) {
return (
<Table
loading={loading}
columns={otherTasksColumns}
rowKey="task_id"
dataSource={items}
/>
);
}
OtherTasksTable.propTypes = TablePropTypes;

View File

@@ -0,0 +1,38 @@
import React from 'react';
import PropTypes from 'prop-types';
import Tabs from 'antd/lib/tabs';
import { PageHeader } from '@/components/PageHeader';
import './layout.less';
export default function Layout({ activeTab, children }) {
return (
<div className="container admin-page-layout">
<PageHeader title="Admin" />
<div className="bg-white tiled">
<Tabs className="admin-page-layout-tabs" defaultActiveKey={activeTab} animated={false}>
<Tabs.TabPane key="system_status" tab={<a href="admin/status">System Status</a>}>
{(activeTab === 'system_status') ? children : null}
</Tabs.TabPane>
<Tabs.TabPane key="tasks" tab={<a href="admin/queries/tasks">Celery Status</a>}>
{(activeTab === 'tasks') ? children : null}
</Tabs.TabPane>
<Tabs.TabPane key="outdated_queries" tab={<a href="admin/queries/outdated">Outdated Queries</a>}>
{(activeTab === 'outdated_queries') ? children : null}
</Tabs.TabPane>
</Tabs>
</div>
</div>
);
}
Layout.propTypes = {
activeTab: PropTypes.string,
children: PropTypes.node,
};
Layout.defaultProps = {
activeTab: 'system_status',
children: null,
};

View File

@@ -0,0 +1,110 @@
/* eslint-disable react/prop-types */
import { toPairs } from 'lodash';
import React from 'react';
import List from 'antd/lib/list';
import Card from 'antd/lib/card';
import { TimeAgo } from '@/components/TimeAgo';
import { toHuman, prettySize } from '@/filters';
export function General({ info }) {
info = toPairs(info);
return (
<Card title="General" size="small">
{(info.length === 0) && (
<div className="text-muted text-center">No data</div>
)}
{(info.length > 0) && (
<List
size="small"
itemLayout="vertical"
dataSource={info}
renderItem={([name, value]) => (
<List.Item extra={<span className="badge">{value}</span>}>
{toHuman(name)}
</List.Item>
)}
/>
)}
</Card>
);
}
export function DatabaseMetrics({ info }) {
return (
<Card title="Redash Database" size="small">
{(info.length === 0) && (
<div className="text-muted text-center">No data</div>
)}
{(info.length > 0) && (
<List
size="small"
itemLayout="vertical"
dataSource={info}
renderItem={([name, size]) => (
<List.Item extra={<span className="badge">{prettySize(size)}</span>}>
{name}
</List.Item>
)}
/>
)}
</Card>
);
}
export function Queues({ info }) {
info = toPairs(info);
return (
<Card title="Queues" size="small">
{(info.length === 0) && (
<div className="text-muted text-center">No data</div>
)}
{(info.length > 0) && (
<List
size="small"
itemLayout="vertical"
dataSource={info}
renderItem={([name, queue]) => (
<List.Item extra={<span className="badge">{queue.size}</span>}>
{name}
</List.Item>
)}
/>
)}
</Card>
);
}
export function Manager({ info }) {
const items = info ? [(
<List.Item extra={<span className="badge"><TimeAgo date={info.lastRefreshAt} placeholder="n/a" /></span>}>
Last Refresh
</List.Item>
), (
<List.Item extra={<span className="badge"><TimeAgo date={info.startedAt} placeholder="n/a" /></span>}>
Started
</List.Item>
), (
<List.Item extra={<span className="badge">{info.outdatedQueriesCount}</span>}>
Outdated Queries Count
</List.Item>
)] : [];
return (
<Card title="Manager" size="small">
{!info && (
<div className="text-muted text-center">No data</div>
)}
{info && (
<List
size="small"
itemLayout="vertical"
dataSource={items}
renderItem={item => item}
/>
)}
</Card>
);
}

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