Compare commits

...

153 Commits

Author SHA1 Message Date
Levko Kravets
435787281a Merge branch 'master' into choropleth-custom-map 2020-02-11 13:38:33 +02:00
Levko Kravets
bc9dd814c9 Optimize Japan Perfectures map (remove irrelevant GeoJson properties) 2020-02-11 13:27:29 +02:00
Levko Kravets
c7b13459e8 Load pre-defined maps directly; move proxy to /api namespace 2020-02-11 12:49:43 +02:00
Levko Kravets
813d97a62c Use proxy to load custom maps (to bypass CSP) 2020-02-11 12:36:19 +02:00
Omer Lachish
ddb0ef15c1 Set default query execution time limit to unlimited (#4626)
* default query execution time limit to 1 hour

* use -1 (run infinitely)  as a default limit
2020-02-11 11:23:02 +02:00
Jannis Leidel
9646156965 Handle stale jobs more carefully before purging them. (#4615) 2020-02-11 11:14:26 +02:00
Jannis Leidel
5c7cb1af3d Update cassandra-driver to 3.21.0. (#4636)
This provides binary wheel files and reduces Docker build times drastically.
2020-02-10 21:48:43 +02:00
Arik Fraimovich
2bd8771188 Fix: no need to encode strings anymore (#4627)
* It's 2020, we got Python 3, no need to encode strings anymore

* Remove encode calls from other places

* use alert.name directly
2020-02-10 20:34:42 +02:00
Arik Fraimovich
86f8f32ab4 Snowflake: switch to simpler query for fetching columns (#4634)
Because we already call USE DATABASE before running SHOW COLUMNS adding IN DATABASE is redundant, but causes an error if the user specifies a schema along with database name.
2020-02-10 20:34:23 +02:00
Gabriel Dutra
fdccaabbe9 Check for LDAP Login in Organization Settings (#4359)
* Check for LDAP Login in Organization Settings

* Restyled by prettier (#4570)

Co-authored-by: Arik Fraimovich <arik@arikfr.com>
Co-authored-by: restyled-io[bot] <32688539+restyled-io[bot]@users.noreply.github.com>
2020-02-10 20:13:56 +02:00
Levko Kravets
2f5920d5e4 getredash/redash#4601 Chart editor: enable search in columns selects (#4602) 2020-02-09 17:38:48 +02:00
Omer Lachish
e97510b2ee Clickhouse: control whether to verify SSL certificate (#4631) 2020-02-09 16:03:43 +02:00
Omer Lachish
80cfa3c557 set correct values for ProxyFix (#4630) 2020-02-09 15:20:16 +02:00
mickeey2525
9b71b569e2 Fix treasuredata endpoint (#4582)
* fix treasuredata endpoint

* make endpoint as a required

* fix unneccessart required parameter
2020-02-09 13:52:44 +02:00
Arik Fraimovich
f2159472da Fix: encode/decode bytestring for base64. (#4624)
* Fix: encode/decode bytestring for base64.

* Apply b64encode fix to HiveHttp
2020-02-09 13:41:41 +02:00
Omer Lachish
c961c33e49 If the error message happens to be empty, it will break serailization. (#4622) 2020-02-09 13:17:43 +02:00
Omer Lachish
5afc94c562 sync_user_details doens't really need a custom ttl (#4625) 2020-02-09 12:57:16 +02:00
Jesse
cee1a07320 Sort schema columns alphabetically (#4595)
* Adds logic to sort column names returned by the query runner. If `sorted`
raises an Exception it returns the column names unaltered from the query
runner.

* Moves table name sorting from model code into schema handler.

* Moves token sorting into the model code.

* Replaces single-quotes with double-quotes for consistency.

* Applies black formatting to changes.

* Moves schema sort into separate method. Adds test.

* Fixes output schema variable name. Without this the sorted cache is never returned!

   ____  ____  ____  _____
  / __ \/ __ \/ __ \/ ___/
 / /_/ / /_/ / /_/ (__  )
 \____/\____/ .___/____/
           /_/

* Adds test case guaranteeing that the model actually _uses_ the schema sorter.

Related to a31f90178c
2020-02-09 12:40:47 +02:00
Eduardo Garcia
42b1eadeb2 Update copyright year to 2020 in LICENSE (#4616) 2020-02-09 12:39:22 +02:00
Omer Lachish
7edac9ca89 keep adhoc job results longer (determined by settings.JOB_EXPIRY_TIME) (#4559) 2020-02-09 12:28:58 +02:00
David Hernández
69893f0304 Force specific version of Werkzeug to prevent the breaking changes of the new release. (#4618) 2020-02-09 09:23:48 +02:00
Jannis Leidel
b089f5f0ef Use correct logger when enqueuing a query execution. (#4614) 2020-02-06 15:16:21 +01:00
Omer Lachish
7a34a76817 RQ: Missing currently executing queries view (#4558)
* add meta information to executing queries

* add a table for running queries

* add pagination to queues table

* sort the queues table

* add pagination to all tables
2020-02-03 23:51:20 +02:00
Levko Kravets
b331c4c922 Improve cache; fix typo 2020-01-30 00:02:19 +02:00
Levko Kravets
6187448e6a Choropleth: fix map "jumping" on load; don't save bounds if user didn't edit them; refine code a bit 2020-01-29 23:36:27 +02:00
Levko Kravets
2de3895986 Query editor: fix shortcuts (#4598) 2020-01-29 21:26:05 +02:00
Levko Kravets
3f280b1f6e Don't handle bounds changes while loading geoJson data 2020-01-29 13:40:03 +02:00
Levko Kravets
3b29f0c0a7 Use cache for geoJson requests 2020-01-29 13:05:54 +02:00
Levko Kravets
4911764663 Keep last custom map URL when selecting predefined map type 2020-01-29 13:05:10 +02:00
Levko Kravets
6260601213 Use separate input for custom map URL (pre-defined map URLs should not be saved in options, only keys) 2020-01-29 12:21:45 +02:00
Levko Kravets
8f7d1d8281 Choropleth: allow to use custom maps 2020-01-29 11:14:08 +02:00
Levko Kravets
713fd2d0fb Change visualizations import to be static (#4592)
* getredash/redash#4565 Change visualizations import to be static

* Move visualizations-related components to own folder
2020-01-28 12:48:38 +02:00
Levko Kravets
19c6d331b6 Refine routes definitions (#4579)
* Refine routes definitions

* Replace HoC wrappers with functions to create route definition

* Some updates for code consistency

* ItemsList component: remove currentRoute dependency

* Prepare route parametes in wrapper functions
2020-01-26 14:53:40 +02:00
Omer Lachish
a36b10173c Fix empty values sent in dynamic form (#3886)
* remove legacy session identifier support

* remove redundant test

* redirect to login to support any invalid session identifiers

* be more specific with caught errors

* reject empty values in DynamicForm

* don't submit form values if they are empty (unless they are
intentionally set to empty string)

* set empty values to null to clear out data source option in the model

* check explicitly for null
2020-01-23 21:21:49 +02:00
Eran Sandler
7d11fae9ea Added support for running MongoDB queries on secondary in replicaset mode (#1424)
* - Added support to specify read preference when query a replicaset database (for example, secondaryPreferred - to try and read data from secondary before primary).
- Removed old code that used MongoClientReplicaSet as it is now just a reference to MongoClient
- Fixed a documentation type :-)

* Moving to PyMongo 3.3.1 which also supports MongoDB 3.2

* Changed the readPreference config to use an enum

* Pass readPreference to MongoClient

* primaryPreferred is now the default
2020-01-23 21:14:37 +02:00
Levko Kravets
35e41385dc Fixes several bugs on dashboard page (see description) (#4571)
* Move each hook to own file; move hooks and components to own folders

* Update URL and timer only when refresh rate changes

* Skip dashboard refresh if previous refresh is still running

* Fix test
2020-01-23 17:03:37 +02:00
Levko Kravets
cdefa847c0 Restore query execute notifications (missed during React migration) (#4577) 2020-01-23 16:21:51 +02:00
Levko Kravets
a90b8c7443 getredash/redash#173 Don't allow to fork query while previous fork is still in progress (#4578) 2020-01-23 16:08:59 +02:00
Levko Kravets
1ba3a23457 Bug: when using global dashboard filters, widgets continuously update their local filters (#4575) 2020-01-23 16:07:28 +02:00
Levko Kravets
8a5e0ea3f4 Refine Timer and TimeAgo components (get rid of force update) (#4580) 2020-01-23 16:06:38 +02:00
Levko Kravets
cbc56264ea React migration cleanup (#4572)
* Revisit ANGULAR_REMOVE_ME things

* Remove styles related to 3rd-party Angular and jQuery libraries

* Remove some more unused styles

* Revisit error handling (app-wide)

* Remove unused file

* CR1
2020-01-22 17:15:25 +02:00
Levko Kravets
c92bb63f8b Fix Map visualization: L.layerGroup cannot compute its bounds (#4573) 2020-01-22 10:11:29 +02:00
Levko Kravets
ff0dbd5f01 Save new query before adding new visualization (#4569) 2020-01-21 13:06:26 +02:00
Leo Palmer Sunmo
80bfd405fd Force saml auth scheme (#3614)
* Add SAML scheme override env var

* Make it pretty, please the linter

* Import settings properly
2020-01-21 11:45:21 +02:00
taminif
945f53fea3 delete variable (#3813)
* delete variable

* delete duplicate code

* add empty line

* delete empty line
2020-01-21 11:29:56 +02:00
Steve Buckingham
56b51be64a Add redshift role use option (#4532)
* Add redshift role use option

* Update requirements for SSL socket wrap issue fixes

* Split Redshift class into User and IAM logins

* Update incorrect register

* Change type names

* Correct class name to inherit

* Render IAM redshift image and field order correct

* Update redash/query_runner/pg.py

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

* Update redash/query_runner/pg.py

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

* Remove need for specified urllib - specify pyopenssl is enough

* Pyopenssl back down to 19.0.0

Co-authored-by: Arik Fraimovich <arik@arikfr.com>
2020-01-21 11:18:33 +02:00
Levko Kravets
a682265e13 Migrate router and <app-view> to React (#4525)
* Migrate router and <app-view> to React: skeleton

* Update layout on route change

* Start moving page routes from angular to react

* Move page routes to react except of public dashboard and visualization embed)

* Move public dashboard and visualization embed routes to React

* Replace $route/$routeParams usages

* Some cleanup

* Replace AngularJS $location service with implementation based on history library

* Minor fix to how ApplicationView handles route change

* Explicitly use global layout for each page instead of handling related stuff in ApplicationArea component

* Error handling

* Remove AngularJS and related dependencies

* Move Parameter factory method to a separate file

* Fix CSS (replace custom components with classes)

* Fix: keep other url parts when updating location partially; refine code

* Fix tests

* Make router work in multi-org mode (respect <base> tag)

* Optimzation: don't resolve route if path didn't change

* Fix search input in header; error handling improvement (handle more errors in pages; global error handler for unhandled errors; dialog dismiss 'unhandled rejection' errors)

* Fix page keys; fix navigateTo calls (third parameter not available)

* Use relative links

* Router: ignore location REPLACE events, resolve only on PUSH/POP

* Fix tests

* Remove unused jQuery reference

* Show error from backend when creating Destination

* Remove route.resolve where not necessary (used constant values)

* New Query page: keep state on saving, reload when creating another new query

* Use currentRoute.key instead of hard-coded keys for page components

* Tidy up Router

* Tidy up location service

* Fix tests

* Don't add parameters changes to browser's history

* Fix test (improved fix)

Co-authored-by: Gabriel Dutra <nesk.frz@gmail.com>
2020-01-20 20:56:37 +02:00
Arik Fraimovich
a891160b4d Upgrade snowflake-connector-python (#4567)
* Upgrade snowflake-connector-python

* Downgrade requests
2020-01-20 19:50:57 +02:00
Gabriel Dutra
086798bbb7 Fix Cypress issues after React version of Query Pages (#4545)
* Update Pivot rows assertion

* Allow replacing Query results with Apply Changes
2020-01-20 14:26:22 +02:00
Levko Kravets
ebcec85c0c Tags filter doesn't work because of wrong query params format (#4563)
* getredash/redash#4557 Tags filter doesn't work because of wrong query params format

* Fix tests
2020-01-20 12:49:17 +02:00
Gabriel Dutra
2b5bfad054 Add padding to QueryExecutionStatus text (#4553) 2020-01-16 21:18:06 +02:00
Gabriel Dutra
479b277b91 Add loading state to Query save button (#4551)
* Add loading state to Query save button

* Hide dirty indication and icon when saving
2020-01-16 14:21:38 +02:00
Arik Fraimovich
94ac11c787 webpack: remove children from output (#4540) 2020-01-14 14:05:37 +02:00
Jannis Leidel
a7ef3ad72a Get rid of six and fix str/unicode types regression that became active on Python 3. (#4533)
This was introduced in d38ca803c5.
2020-01-14 12:51:36 +02:00
Ari Ekmekji
afe8c95f4d Load collections in all workspaces (#4541) 2020-01-14 12:48:04 +02:00
Omer Lachish
fe06f7f63e Google Analytics runner - iterate over keys the Python 3 way (#4538)
* iterate over key names instead of dict_keys values

* use dict comprehension instead of manipulating existing dict
2020-01-13 14:41:30 +02:00
Omer Lachish
5e01211852 adjust imports to match influxdb 5.2.3 (#4531) 2020-01-13 10:43:29 +02:00
Gabriel Dutra
375ffd3250 Migrate services and replace $http with axios (#4497) 2020-01-12 22:25:26 -03:00
Omer Lachish
674f057c59 fix typo in azure kusto runner (#4537) 2020-01-12 22:45:32 +02:00
Omer Lachish
aa17681af2 Nuke Celery (#4521)
* enforce hard limits on non-responsive work horses by workers

* move differences from Worker to helper methods to help make the specialization clearer

* move HardLimitingWorker to redash/tasks

* move schedule.py to /tasks

* explain the motivation for HardLimitingWorker

* pleasing CodeClimate

* pleasing CodeClimate

* port query execution to RQ

* get rid of argsrepr

* avoid star imports

* allow queries to be cancelled in RQ

* return QueryExecutionErrors as job results

* fix TestTaskEnqueue and QueryExecutorTests

* remove Celery monitoring

* get rid of QueryTask and use RQ jobs directly (with a job serializer)

* Revert "remove Celery monitoring"

This reverts commit 37a74ea403.

* reduce occurences of the word 'task'

* use Worker, Queue and Job instead of spreading names that share behavior details

* remove locks for failed jobs as well

* did I not commit that colon? oh my

* push the redis connection to RQ's stack on every request to avoid verbose connection setting

* use a connection context for tests

* remove Celery monitoring

* 👋 Celery

* remove Celery from Cypress

* black it up

* some more black

* return all started/queued job ids (for future monitoring

* Restyled by prettier (#4522)

* remove celery.py

* remove some frontend residuals that reappeared after a merge

Co-authored-by: restyled-io[bot] <32688539+restyled-io[bot]@users.noreply.github.com>
2020-01-12 22:36:48 +02:00
Takuya Arita
13c3531956 Update description for the new setup repository (#4535) 2020-01-12 15:22:10 +02:00
Gabriel Dutra
350716c525 Add maildev missing settings (#4527) 2020-01-10 09:29:33 +02:00
Gabriel Dutra
fe11b8cc35 Cypress: Add test for Settings Tabs (#4530) 2020-01-09 12:40:40 -03:00
Gabriel Dutra
465dbc03b7 Hide unavailable page links to non-admin users in settings and header (#4524)
* Filter unavailable menu items in SettingsWrapper

* Don't show Alert Destination in header to users
2020-01-08 10:59:59 +02:00
Gabriel Dutra
76f0dcb085 Replace angular-sanitize with DOMPurify (#4502)
* Switch angular-sanitize package with dompurify

* Replace $sanitize with DOMPurify.sanitize

Co-authored-by: Arik Fraimovich <arik@arikfr.com>
2020-01-08 10:56:11 +02:00
Gabriel Dutra
99c276fc9a Migrate Query pages to React (#4429)
* Migrate Query Source View page to React: skeleton

* Sync QueryView and QuerySource (#4430)

* Migrate schema browser to react (#4432)

* Restyle code with Prettier

* Migrate Query page to React: Save changes (#4452)

* Migrate query source to React: Set of updates (#4457)

* Migrate Query page to React: Visualization Tabs (#4453)

Co-Authored-By: Levko Kravets <levko.ne@gmail.com>

* Migrate Query Source page to React: Visualizations area (#4463)

* Migrate Query page to React: Delete visualization button (#4461)

* Migrate Query Source page to React: Visualization actions (#4467)

* Migrate Query pages to React: Execute query hook (#4470)

* Migrate Query Source page to React: Editor area (#4468)

* Migrate Query Source page to React: metadata, schedule and description blocks (#4476)

* Migrate Query page to React: Cancel query execution (#4496)

* Migrate Query Source page to React: refine code (#4499)

* Migrate Query Source page to React: alerts (#4504)

* Migrate Query Source page to React: unsaved changes alert (#4505)

* Migrate Query Source to React: resizable areas (v2) (#4503)

* Migrate Query page to React: Query View (#4455)

Co-authored-by: Levko Kravets <levko.ne@gmail.com>

* Switch React and Angular versions of pages (until Angular version removed)

* Migrate Query pages to React: fix permissions (#4506)

* Migrate Query Source page to React: don't reload when saving new query (#4507)

* Migrate Query pages to React: fix tests (#4509)

* Use skipParametersDirtyFlag in executeQuery

* Fix: cannot fork query from Query View page

* Optimize query editor: handle query text changes faster

* Revert "Optimize query editor: handle query text changes faster"

This reverts commit 2934e53be6.

* Reduce debounced time to 100

* Migrate Query pages to React: cleanup (#4512)

* Migrate Query pages to React: cleanup

* Further cleanup

* Remove unused dependencies

* Fix embed pages

* Attempt to fix flaky test

* Cleanup: explicitly register the last Angular component

* Move contents of /filters folder to /lib

* Remove unnecessary import

* Remove cy.wait from Parameters spec

Co-authored-by: Gabriel Dutra <nesk.frz@gmail.com>

Co-authored-by: Levko Kravets <levko.ne@gmail.com>
2020-01-06 20:51:45 +02:00
Levko Kravets
fc9e8fe2aa Add error boundary to catch errors in visualizations (#4518)
* Add error boundary to catch errors in visualizations

* Fix: Funnel crash when step column is date/time

* CR1

* CR2
2020-01-06 10:22:20 +02:00
Omer Lachish
260bfca767 Multiprocess RQ workers (using supervisor) (#4371)
* launch and monitor multiple workers using supervisor

* run supervisord in non-daemon mode

* redirect all output to stdout/stderr

* no need to log supervisord's output because it is redirected to stdout anyway

* updated and less brittle healthcheck

* add supervisor healthchecks

* remove redundant supervisor installation as it is installed by pip

* add a 5 minute check gate
2020-01-01 15:32:29 +02:00
Arik Fraimovich
f85490cf50 Fix: don't try to access message property of an exception (#4516)
(not supported in Python 3)
2019-12-31 12:45:29 +02:00
Gabriel Dutra
29582e3212 Run prettier on cypress folder (#4510)
* Run prettier on cypress folder

* Test Restyled

* Revert "Test Restyled"

This reverts commit 13d43968fe.
2019-12-30 19:40:56 +02:00
Omer Lachish
329e85987c Execute Queries in RQ (#4413)
* enforce hard limits on non-responsive work horses by workers

* move differences from Worker to helper methods to help make the specialization clearer

* move HardLimitingWorker to redash/tasks

* move schedule.py to /tasks

* explain the motivation for HardLimitingWorker

* pleasing CodeClimate

* pleasing CodeClimate

* port query execution to RQ

* get rid of argsrepr

* avoid star imports

* allow queries to be cancelled in RQ

* return QueryExecutionErrors as job results

* fix TestTaskEnqueue and QueryExecutorTests

* remove Celery monitoring

* get rid of QueryTask and use RQ jobs directly (with a job serializer)

* Revert "remove Celery monitoring"

This reverts commit 37a74ea403.

* reduce occurences of the word 'task'

* use Worker, Queue and Job instead of spreading names that share behavior details

* remove locks for failed jobs as well

* did I not commit that colon? oh my

* push the redis connection to RQ's stack on every request to avoid verbose connection setting

* use a connection context for tests

* black it up

* run RQ on all queues when running in Cypress
2019-12-30 14:11:01 +02:00
Arik Fraimovich
ff34dedf46 Fix: properly encode UTF-8 filenames in query results request (#4498)
* Fix: properly encode UTF-8 filenames in query results request

Ended up copying the implementation from Flask's send_file helper function, because send_file doesn't really fit our use case.

* Update tests/handlers/test_query_results.py

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

Co-authored-by: Omer Lachish <omer@rauchy.net>
2019-12-30 11:52:18 +02:00
Arik Fraimovich
d0fb377ed6 Viz Embed: Add option to hide timestamp (#4491) 2019-12-30 11:45:22 +02:00
Arik Fraimovich
30bc1e2ff6 Refine permissions usage in Redash to allow for guest users (#4492)
* Allow executing query with either view_query or execute_query permissions.

* Render AuthHeader according to permissions.

* Don't return dashboards where you only have access to textbox widget.

Closes #4099.
2019-12-30 10:07:20 +02:00
Gabriel Dutra
fd46194580 Update EditInPlace to use Antd components (#4493) 2019-12-26 12:53:33 -03:00
deecay
f5900a1929 Chart: Bubble size control by coefficient and sizemode (#3928) 2019-12-26 16:19:45 +02:00
Tsuyoshi Yoshizawa
c2b39db03e Support download as TSV File (#4445) 2019-12-26 16:16:48 +02:00
Omer Lachish
f420e02cee adjust imports to match simple-salesforce 0.74.3 (#4490) 2019-12-25 16:26:58 +02:00
Arik Fraimovich
0aa176e2e5 Don't update query's updated_at when updating schedule_failures counter (#4488) 2019-12-25 16:25:16 +02:00
Arik Fraimovich
97d523e348 Retain tags when forking a query (#4489) 2019-12-25 16:25:02 +02:00
Arik Fraimovich
88d21e9461 Add explicit handling of 404 errors in query result requests. (#4487) 2019-12-25 15:46:13 +02:00
Arik Fraimovich
40c1ef0f59 Fix: query results query runner fails to load cached results. (#4486) 2019-12-25 15:21:43 +02:00
Arik Fraimovich
10ba2ddbaa Snowflake: add missing date types (#4484)
Without those the values might be miscategorized in the UI.
2019-12-25 14:58:45 +02:00
Omer Lachish
e7eedd0556 fix all occurances of B306: BaseException.message has been deprecated as of Python 2.6 and is removed in Python 3. Use str(e) to access the user-readable message. Use e.args to access arguments passed to the exception. (#4482) 2019-12-25 10:13:39 +02:00
Omer Lachish
c3299ff0ad totalRows are returned as a string and should be a number (#4481) 2019-12-24 22:20:17 +02:00
Randy Zwitch
6b2f23f357 Update pymapd to 0.19.0 (#4424) 2019-12-24 15:34:42 +02:00
Arik Fraimovich
0819f80e72 Hive/Databricks: mark date types as TYPE_DATE. (#4419) 2019-12-24 10:39:56 +02:00
Gabriel Dutra
7223f60ddf Migrate VisualizationEmbed to React (#4364)
* Migrate VisualizationEmbed to React

* Angular cleanup

* Remove onClick event from TimeAgo

* Check Table exists before taking snapshot

* Apply Prettier
2019-12-24 10:21:48 +02:00
Gabriel Dutra
38b6b47594 Migrate Dashboard and Public Dashboard to React (#4228)
* Initial React Rendering with useDashboard

* Make sure widgets refresh + useCallback

* Rename collectFilters and add refreshRate

* Fix error updates not being rendered

* Only render widget bottom when queryResults exists

* Cleanup

* Add useCallback to refreshDashboard

* Make sure Promise.all have all promises done

* Start migrating Dashoard to React
- initial rendering
- some actions
- temporary updated less file

* Fullscreen handler added

* Separate refreshRateHandler hook

* Add a few tooltips

* Separate DashboardControl and normalize btn width

* Share Button

* Fix serach params not updating

* Enumerate More Options

* Toggle Publish options

* Archive Dashboard

* Parameters + Filters

* Prepare Manage Permissions

* Start to create edit mode

* Add Edit Mode functionalities

* Use previous state when updating dashboard

* Mobile adjustments

* PermissionsEditorDialog + Dashboard page title

* Update Dashboard spec

* Fix other specs

* Break dashboard.less

* Hide publish button on mobile

* Angular Cleaning

* Keep edit state when changing resolution

* Bug fix: Dashboard Level Filters not updating

* Remove prepareWidgetsForDashboard

* Revert "Remove prepareWidgetsForDashboard"

This reverts commit b434f03da1.

* Avoid saving layout changes out of editing mode

* Apply policy for enabled refresh rates

* Disable loadDashboard deps

* Restyled by prettier (#4459)

* Update title when dashboard name updates

Co-authored-by: restyled-io[bot] <32688539+restyled-io[bot]@users.noreply.github.com>
2019-12-24 10:20:40 +02:00
Levko Kravets
49dcb7f689 Refactor QueryEditor component (#4464) 2019-12-20 15:35:43 +02:00
deecay
425e79fdd2 Fix prettier commandline option to be recursive (#4458) 2019-12-17 14:52:45 +02:00
Levko Kravets
bc52b78889 Third column not selectable for Bubble and Heatmap charts (#4449) 2019-12-16 13:00:17 +02:00
deecay
944adb95ba Map: add tooltip and popup templating (#4443) 2019-12-14 20:07:09 +02:00
Daniel Dubovski
8cb49158bf Adding application to Azure Kusto query runner (#4441)
This is to allow for better metrics collection and tracking on the service side.
More info can be found [here](https://docs.microsoft.com/en-us/azure/kusto/api/netfx/request-properties#the-application-x-ms-app-named-property)
2019-12-13 21:47:11 +02:00
Gabriel Dutra
ca098172e9 Fix Restyled config (#4438) 2019-12-11 23:06:17 -03:00
Omer Lachish
a3beac0b78 allow setting of custom sentry environments (#4437) 2019-12-11 23:00:06 +02:00
Arik Fraimovich
56d3be2248 Prettier all the Javascript code & GitHub Action (#4433)
* Prettier all the JS files

* Add GitHub Action to autoformat code pushed to master

* Fix eslint violation due to formatting.

* Remove GitHub actions for styling

* Add restyled.io config
2019-12-11 17:05:38 +02:00
Arik Fraimovich
81b14a58ef Remove Husky (#4435) 2019-12-11 14:49:57 +02:00
Arik Fraimovich
2dff8b9a00 Black support for the Python codebase (#4297)
* Apply black formatting

* Add auto formatting when committing to master

* Update CONTRIBUTING.md re. Black & Prettier
2019-12-11 13:54:29 +02:00
Arik Fraimovich
37a964c8d9 Remove codeclimate config (#4434) 2019-12-11 13:44:52 +02:00
Arik Fraimovich
1b9b3032ca Change eslint configuration and fix resulting issues (#4423)
* Remove app/service/query-string (unused) and its dependency.

* Fix usage of mixed operators.

* eslint --fix fixes for missing dependencies for react hooks

* Fix: useCallback dependency passed to $http's .catch.

* Satisfy react/no-direct-mutation-state.

* Fix no-mixed-operators violations.

* Move the decision of whether to render Custom chart one level up to make sure hooks are called in the same order.

* Fix: name was undefined. It wasn't detected before because there is such global.

* Simplify eslint config and switch to creat-react-app's eslint base.

* Add prettier config.

* Make sure eslint doesn't conflict with prettier

* A few updates post eslint (#4425)

* Prettier command in package.json
2019-12-11 12:00:46 +02:00
David Mudro
0385b6fb64 Fix counter vizualization (#4385)
* crude unit tests for counter visualisation utils

* improve type safety with default param values for getCounterData()

* fix count rows never shows zero

* remove default values for getCounterData() params
2019-12-10 13:38:22 +02:00
Arik Fraimovich
7c05a730dc Remove --max-old-space-size=4096 from npm build command (#4381)
* Remove --max-old-space-size=4096 from build

Looks like it's no longer needed.

* Update to node v12.

* Add build:old-node-version for those who have Node < 12.
2019-12-05 22:41:57 +02:00
Kenji Ichihashi
263305214e Update rds-combined-ca-bundle.pem(#4290) (#4304)
Can use rds-ca-2019 and etc
`$ curl https://s3.amazonaws.com/rds-downloads/rds-combined-ca-bundle.pem \
> redash/query_runner/files/rds-combined-ca-bundle.pem`
2019-12-05 11:29:42 +02:00
Jakdaw
3494e21cf4 Add user/pass authentication support for Druid (#4315)
* Add support for configuring a Username/Password for the connection to Druid

* Bump pydruid version for username/password support

* Deal with missing/empty configuration parameters
2019-12-05 09:27:59 +02:00
Gabriel Dutra
15e8b88996 Cypress: Make sure params are saved before reload (#4420) 2019-12-04 13:37:31 -03:00
Levko Kravets
ba36b4e671 Migrate AddToDashboard dialog to React (#4408) 2019-12-04 17:50:50 +02:00
Levko Kravets
94bd03dc42 Set of improvements and refinements to visualizations after React migration (#4382) 2019-12-04 16:23:29 +02:00
Levko Kravets
041d05d18b Chart series switch places when picking Y axis or color (#4412) 2019-12-04 16:00:01 +02:00
Gabriel Dutra
c14e7ab4ca Fix dragged parameter wrapping in some cases (#4415) 2019-12-03 12:48:11 -03:00
Monica Gangwar
4d6c30ef13 refreshing snowflake schema w/o waking cluster (#4285)
* refreshing snowflake schema w/o waking cluster

Have also added a new internal method to not select a
warehouse while executing query
Using 'show columns' to fetch database schema instead of
executing a select query in information schema
show columns does not require a warehouse to run

* modularising snowflake code to avoid repetitions

fixing internal function syntax and avoiding
code repetition

* removing user object in snowflake schema query
2019-12-02 10:48:30 +02:00
Arik Fraimovich
36ab8eae89 Update Snowflake connector version to address compatibility issue with Azure dependencies (#4407) 2019-11-27 18:44:08 +02:00
Stefan Mees
e82373ac1d add pyexasol datasource, ensure that integer dont overflow in javascript (#4378) 2019-11-27 18:43:58 +02:00
Arik Fraimovich
d3feba69b2 Downgrade Kombu version to 4.6.3 (#4406)
It was accidentally upgraded as part of the dependencies upgrade we did recently, but 4.6.5 has a bug...
2019-11-27 18:08:47 +02:00
Omer Lachish
80f3ec1c99 avoid logging job parameters (#4311) 2019-11-27 09:33:20 +02:00
Arik Fraimovich
c612bba19c Amazon CloudWatch query runners (#4372)
* CloudWatch Metrics query runner

* Add: query runner for CloudWatch Logs Insights

* Add logos

* Update Insights type

* Basic test connection

* Format files
2019-11-27 09:14:28 +02:00
Nicolas Le Manchet
f5a40827aa Remove builtins invalid in Python 3 from Python runner (#4375)
These few builtins were available in Python 2.7 but not anymore
in Python 3, making the runner fail to start.
2019-11-27 09:12:36 +02:00
Gabriel Dutra
5de291a98d Fix Map spec and Alert Page snapshot flakyness (#4403) 2019-11-26 19:08:18 -03:00
Levko Kravets
c70a48db9c Table visualization with column named "children" renders +/- buttons (#4394) 2019-11-26 15:47:19 +02:00
Omer Lachish
be56035bd6 don't try to purge jobs which have already been deleted (#4396) 2019-11-25 11:04:00 +02:00
Gabriel Dutra
7c97d8eafa Add autoscroll to ng-view (#4337) 2019-11-24 14:01:35 -03:00
Gabriel Dutra
0563ecf648 Migrate Home to React (#4379) 2019-11-24 13:59:56 -03:00
Levko Kravets
e72d7a8cca Table visualization: accept timestamp for date/time columns (#4389) 2019-11-24 11:50:52 +02:00
Levko Kravets
a7a933946b Hide deprecated visualizations from query editor (#4388)
* Hide deprecated visualizations from query editor

* Fix Map tests
2019-11-24 11:05:01 +02:00
uncletimmy3
7cfd362a7a fix typo at unsupportedRedirect.js (#4387) 2019-11-24 10:46:16 +02:00
Arik Fraimovich
4d1b359713 Remove unused npm dependencies (#4380)
* Remove ui-ace.

* Remove ui-sortable.

* Remove angular-base64-upload.

* Remove angular-messages.

* Remove jquery-ui.

* Update package-lock.json.
2019-11-21 12:18:33 +02:00
Levko Kravets
818649bbec Migrate Chart visualization to React Part 2: Editor (#4139) 2019-11-20 21:57:12 +02:00
Levko Kravets
c6a2725f0a Migrate Map visualization to React (#4278) 2019-11-20 17:36:59 +02:00
Gabriel Dutra
5cd6913e40 Fix Cypress and Percy flakyness issues (#4365) 2019-11-18 10:37:01 -03:00
Gabriel Dutra
0aebb37317 Remove Chrome Logger and update Cypress and Percy (#4354) 2019-11-14 15:23:00 -03:00
Levko Kravets
aa06b32e17 Add some tests for Choropleth visualization (#4358) 2019-11-14 19:08:51 +02:00
Levko Kravets
b44fa51829 Migrate Funnel visualization to React (#4267)
* Migrate Funnel visualization to React: Editor

* Migrate Funnel visualization to React: Renderer

* Replace Auto sort options with Sort Column + Reverse Order

* Add option for items limit (instead of hard-coded value)

* Add number formatting options

* Replace d3.max with lodash.maxBy; fix bug in prepareData

* Add options for min/max percent values

* Debounce inputs

* Tests

* Refine Renderer: split components, use Ant Table for rendering, fix data handling

* Extract utility function to own file

* Fix tests

* Fix: sometimes after updating options, funnel shows "ghost" rows from previous dataset

* Sort by value column by default
2019-11-14 15:47:17 +02:00
Levko Kravets
1a95904ffd Migrate Choropleth visualization to React (#4313)
* Migrate Choropleth to React: skeleton

* Migrate Choropleth to React: Editor - skeleton

* Choropleth Editor: Bounds tab

* Choropleth Editor: Colors tab

* Choropleth Editor: Format tab

* Choropleth Editor: General tab

* Some refinements

* Migrate Choropleth to React: Renderer

* Refine code

* CR1
2019-11-14 15:42:15 +02:00
Omer Lachish
ef56e4e920 use to set the hash instead of directly manipulating it (#4351)
* use  to set the hash instead of directly manipulating it

* Update Jobs.jsx
2019-11-13 15:36:04 +02:00
shinsuke-nara
d5a3f0de57 CLI command to reencrypt data source options (#4190)
* Script to reencrypt data source options.

* Implement reencrypt sub command under database command.
2019-11-13 15:27:20 +02:00
Arik Fraimovich
cf274d96c8 Fix: number based alerts evaluation isn't working (#4295)
* Fix: correctly evaluate numeric thresholds

* Missing import

* More missing imports

* Alert evaluation: support for booleans
2019-11-13 15:11:21 +02:00
Levko Kravets
c00410768c Migrate Cohort visualization to React (#4270)
* Migrate Cohort to React: Editor

* Extract prepareData and getOptions to own files

* Refine CohortRenderer Angular component (js, less, prepareData) for easier migration

* Migrate Cohort to React: Renderer

* Migrate Cornelius to React: styles

* Migrate Cohort to React: Cornelius library

* Cornelius: add licence info; remove unused style

* Cornelius: use numeral to format numbers; revisit styles

* Cornelius: use moment to format date labels

* Cornelius: use chroma for cell backgrounds; update options; update proptypes; minor fixes

* Tidy up

* Tests
2019-11-13 14:39:08 +02:00
Jakdaw
dda5a9d58f Fix the DB migration so that the correct key is used for encrypting DS credentials. (#4344)
Without this upgrades from at least v5 (and earlier) won't work.
2019-11-11 21:49:05 +02:00
Omer Lachish
a0a32be3dd Admin status page's current tab does not preserve (#4299)
* handle a console warning about passing in string page options

* preserve selected tab in the location hash
2019-11-11 12:04:32 +02:00
Omer Lachish
e0e94d79ac Restarting rq-scheduler reschedules all periodics (#4302)
* add some logging to scheduler

* schedule jobs only if they are not already scheduled

* jobs scheduled with an interval over 24 hours were not repeated

* schedule version_check using standard scheduling

* clean up old jobs that are not part of the definition anymore

* add some tests

* add one more test to verify that reschedules are not done when not neccesary

* no need to check for func existence - all jobs have a func to run
2019-11-11 09:54:41 +02:00
Omer Lachish
f19d24287e auto-refresh data RQ jobs admin page (#4298) 2019-11-11 09:42:05 +02:00
Gabriel Dutra
80878abf7b Migrate Settings Screen to React (#4323)
* Migrate settings-screen to React

* Use black instead of blue color for active item

* Revert "Use black instead of blue color for active item"

This reverts commit 0e4ececa6a.

* Add selectable=false to the Menu
2019-11-10 09:07:40 +02:00
Gabriel Dutra
6716bb390c Update TagsList and Sidebar to use Ant components (#4338) 2019-11-07 13:41:15 -03:00
Omer Lachish
a33d11de3a RQ: periodically clear failed jobs (#4306)
* add some logging to scheduler

* clean failed RQ job data from Redis

* move stale job purging to tasks/general.py

* provide better documentation on why we don't reject keys in FailedJobRegistry at the moment

* pleasing the CodeClimate overlords

* simplified clenaup by deleting both job data and registry entry

* use FailedJobRegistry as source of truth for purging

* remove redundant key deletion

* Update redash/settings/__init__.py

Co-Authored-By: Arik Fraimovich <arik@arikfr.com>
2019-11-07 17:00:53 +02:00
Omer Lachish
6f791a092b Adjust RQ job priorities (#4301)
* prioritize periodic jobs

* declare default queues in inside worker()

* separate send_email to its own queue
2019-11-06 13:36:27 +02:00
Kyle Krueger
cce6546a62 Feature/last x days parameter (#4333)
* Add last 14, 30, 60, and 90 days to DRP.js

Date Range Parameter (DRP)

* Add last 14, 30, 60, and 60 day params to DRP.jsx

DateRangeParameters (DRP)
2019-11-05 16:15:11 +02:00
Ran Byron
5fd78fdb23 New feature - Alert muting (#4276)
* New feature - Alert muting

* pep8 fix

* Fixed backend api update

* whoops semicolon

* Implemented mute
2019-11-02 14:54:26 +02:00
Gabriel Dutra
74dbb8acf3 Skip favorites dropdown loading state on init (#4318) 2019-10-31 13:28:28 -03:00
Omer Lachish
36638be1dd optimize work horse initialization by configuration mappers on the worker process (#4314) 2019-10-30 09:53:06 +02:00
Gabriel Dutra
82f488d231 Migrate PermissionsEditor to React (#4266)
Co-Authored-By: Arik Fraimovich <arik@arikfr.com>
2019-10-29 12:42:31 -03:00
Arik Fraimovich
7b3943052e Move the setup scripts to their own home (#4310) 2019-10-28 21:11:21 +02:00
Arik Fraimovich
96a95b7090 Add V8 to the CHANGELOG. 2019-10-28 13:27:34 +02:00
Omer Lachish
accf0f7ac5 show more workers per page. also allow page size selection (#4300) 2019-10-28 09:51:57 +02:00
852 changed files with 38474 additions and 31146 deletions

View File

@@ -2,7 +2,7 @@ version: '3'
services:
server:
build: ../
command: dev_server
command: server
depends_on:
- postgres
- redis
@@ -31,26 +31,12 @@ services:
REDASH_LOG_LEVEL: "INFO"
REDASH_REDIS_URL: "redis://redis:6379/0"
REDASH_DATABASE_URL: "postgresql://postgres@postgres/postgres"
QUEUES: "default periodic schemas"
celery_worker:
build: ../
command: celery_worker
depends_on:
- server
environment:
PYTHONUNBUFFERED: 0
REDASH_LOG_LEVEL: "INFO"
REDASH_REDIS_URL: "redis://redis:6379/0"
REDASH_DATABASE_URL: "postgresql://postgres@postgres/postgres"
QUEUES: "queries,scheduled_queries"
WORKERS_COUNT: 2
cypress:
build:
context: ../
dockerfile: .circleci/Dockerfile.cypress
depends_on:
- server
- celery_worker
- worker
- scheduler
environment:

View File

@@ -1,32 +0,0 @@
version: "2"
checks:
complex-logic:
enabled: false
file-lines:
enabled: false
method-complexity:
enabled: false
method-count:
enabled: false
method-lines:
config:
threshold: 100
nested-control-flow:
enabled: false
identical-code:
enabled: false
similar-code:
enabled: false
plugins:
pep8:
enabled: true
eslint:
enabled: false
exclude_patterns:
- "tests/**/*.py"
- "migrations/**/*.py"
- "setup/**/*"
- "bin/**/*"
- "**/node_modules/"
- "client/dist/"
- "**/*.pyc"

61
.restyled.yaml Normal file
View File

@@ -0,0 +1,61 @@
enabled: true
auto: false
# Open Restyle PRs?
pull_requests: true
# Leave comments on the original PR linking to the Restyle PR?
comments: true
# Set commit statuses on the original PR?
statuses:
# Red status in the case of differences found
differences: true
# Green status in the case of no differences found
no_differences: true
# Red status if we encounter errors restyling
error: true
# Request review on the Restyle PR?
#
# Possible values:
#
# author: From the author of the original PR
# owner: From the owner of the repository
# none: Don't
#
# One value will apply to both origin and forked PRs, but you can also specify
# separate values.
#
# request_review:
# origin: author
# forked: owner
#
request_review: author
# Add labels to any created Restyle PRs
#
# These can be used to tell other automation to avoid our PRs.
#
labels: ["Skip CI"]
# Labels to ignore
#
# PRs with any of these labels will be ignored by Restyled.
#
# ignore_labels:
# - restyled-ignore
# Restylers to run, and how
restylers:
- name: black
include:
- redash
- tests
- migrations/versions
- name: prettier
include:
- client/app/**/*.js
- client/app/**/*.jsx
- client/cypress/**/*.js

View File

@@ -1,5 +1,9 @@
# Change Log
## v8.0.0 - 2019-10-27
There were no changes in this release since `v8.0.0-beta.2`. This is just to mark a stable release.
## v8.0.0-beta.2 - 2019-09-16
This is an update to the previous beta release, which includes:

View File

@@ -46,8 +46,8 @@ When creating a new bug report, please make sure to:
If you would like to suggest an enhancement or ask for a new feature:
- Please check [the roadmap](https://trello.com/b/b2LUHU7A/redash-roadmap) for existing Trello card for what you want to suggest/ask. If there is, feel free to upvote it to signal interest or add your comments.
- If there is no existing card, open a thread in [the forum](https://discuss.redash.io/c/feature-requests) to start a discussion about what you want to suggest. Try to provide as much details and context as possible and include information about *the problem you want to solve* rather only *your proposed solution*.
- Please check [the forum](https://discuss.redash.io/c/feature-requests/5) for existing threads about what you want to suggest/ask. If there is, feel free to upvote it to signal interest or add your comments.
- If there is no open thread, you're welcome to start one to have a discussion about what you want to suggest. Try to provide as much details and context as possible and include information about *the problem you want to solve* rather only *your proposed solution*.
### Pull Requests
@@ -55,9 +55,9 @@ If you would like to suggest an enhancement or ask for a new feature:
- Include screenshots and animated GIFs in your pull request whenever possible.
- Please add [documentation](#documentation) for new features or changes in functionality along with the code.
- Please follow existing code style:
- Python: we use PEP8 for Python.
- Javascript: we use Airbnb's style guides for [JavaScript](https://github.com/airbnb/javascript#naming-conventions) and [React](https://github.com/airbnb/javascript/blob/master/react) (currently we don't follow Airbnb's convention for naming files, but we're gradually fixing this). To make it automatic and easy, we recommend using [Prettier](https://github.com/prettier/prettier).
- Python: we use [Black](https://github.com/psf/black) to auto format the code.
- Javascript: we use [Prettier](https://github.com/prettier/prettier) to auto-format the code.
### Documentation
The project's documentation can be found at [https://redash.io/help/](https://redash.io/help/). The [documentation sources](https://github.com/getredash/website/tree/master/src/pages/kb) are hosted on GitHub. To contribute edits / new pages, you can use GitHub's interface. Click the "Edit on GitHub" link on the documentation page to quickly open the edit interface.
@@ -66,9 +66,9 @@ The project's documentation can be found at [https://redash.io/help/](https://re
### Release Method
We publish a stable release every ~2 months, although the goal is to get to a stable release every month. You can see the change log on [GitHub releases page](https://github.com/getredash/redash/releases).
We publish a stable release every ~3-4 months, although the goal is to get to a stable release every month.
Every build of the master branch updates the latest *RC release*. These releases are usually stable, but might contain regressions and therefore recommended for "advanced users" only.
Every build of the master branch updates the *redash/redash:preview* Docker Image. These releases are usually stable, but might contain regressions and therefore recommended for "advanced users" only.
When we release a new stable release, we also update the *latest* Docker image tag, the EC2 AMIs and GCE images.

View File

@@ -1,4 +1,4 @@
FROM node:10 as frontend-builder
FROM node:12 as frontend-builder
WORKDIR /frontend
COPY package.json package-lock.json /frontend/

View File

@@ -1,4 +1,4 @@
Copyright (c) 2013-2019, Arik Fraimovich.
Copyright (c) 2013-2020, Arik Fraimovich.
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,

View File

@@ -1,15 +1,6 @@
#!/bin/bash
set -e
celery_worker() {
WORKERS_COUNT=${WORKERS_COUNT:-2}
QUEUES=${QUEUES:-queries,scheduled_queries}
WORKER_EXTRA_OPTIONS=${WORKER_EXTRA_OPTIONS:-}
echo "Starting $WORKERS_COUNT workers for queues: $QUEUES..."
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() {
echo "Starting RQ scheduler..."
@@ -25,7 +16,10 @@ dev_scheduler() {
worker() {
echo "Starting RQ worker..."
exec /app/manage.py rq worker $QUEUES
export WORKERS_COUNT=${WORKERS_COUNT:-2}
export QUEUES=${QUEUES:-}
supervisord -c worker.conf
}
dev_worker() {
@@ -34,15 +28,6 @@ dev_worker() {
exec watchmedo auto-restart --directory=./redash/ --pattern=*.py --recursive -- ./manage.py rq worker $QUEUES
}
dev_celery_worker() {
WORKERS_COUNT=${WORKERS_COUNT:-2}
QUEUES=${QUEUES:-queries,scheduled_queries}
echo "Starting $WORKERS_COUNT workers for queues: $QUEUES..."
exec watchmedo auto-restart --directory=./redash/ --pattern=*.py --recursive -- /usr/local/bin/celery worker --app=redash.worker -c$WORKERS_COUNT -Q$QUEUES -linfo --max-tasks-per-child=10 -Ofair
}
server() {
# 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}
@@ -54,10 +39,6 @@ create_db() {
exec /app/manage.py database create_tables
}
celery_healthcheck() {
exec /usr/local/bin/celery inspect ping --app=redash.worker -d celery@$HOSTNAME
}
rq_healthcheck() {
exec /app/manage.py rq healthcheck
}
@@ -69,13 +50,10 @@ help() {
echo ""
echo "server -- start Redash server (with gunicorn)"
echo "celery_worker -- start Celery worker"
echo "dev_celery_worker -- start Celery worker process which picks up code changes and reloads"
echo "worker -- start a single RQ worker"
echo "dev_worker -- start a single RQ worker with code reloading"
echo "scheduler -- start an rq-scheduler instance"
echo "dev_scheduler -- start an rq-scheduler instance with code reloading"
echo "celery_healthcheck -- runs a Celery healthcheck. Useful for Docker's HEALTHCHECK mechanism."
echo "rq_healthcheck -- runs a RQ healthcheck that verifies that all local workers are active. Useful for Docker's HEALTHCHECK mechanism."
echo ""
echo "shell -- open shell"
@@ -114,14 +92,6 @@ case "$1" in
shift
dev_scheduler
;;
celery_worker)
shift
celery_worker
;;
dev_celery_worker)
shift
dev_celery_worker
;;
dev_worker)
shift
dev_worker
@@ -130,10 +100,6 @@ case "$1" in
shift
rq_healthcheck
;;
celery_healthcheck)
shift
celery_healthcheck
;;
dev_server)
export FLASK_DEBUG=1
exec /app/manage.py runserver --debugger --reload -h 0.0.0.0

View File

@@ -10,7 +10,6 @@
"@babel/preset-react"
],
"plugins": [
"angularjs-annotate",
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-object-assign",
["babel-plugin-transform-builtin-extend", {

View File

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

View File

@@ -1,11 +1,10 @@
module.exports = {
root: true,
extends: ["airbnb", "plugin:compat/recommended"],
extends: ["react-app", "plugin:compat/recommended", "prettier"],
plugins: ["jest", "compat", "no-only-tests"],
settings: {
"import/resolver": "webpack"
},
parser: "babel-eslint",
env: {
browser: true,
node: true
@@ -13,54 +12,6 @@ module.exports = {
rules: {
// allow debugger during development
"no-debugger": process.env.NODE_ENV === "production" ? 2 : 0,
"no-param-reassign": 0,
"no-mixed-operators": 0,
"no-underscore-dangle": 0,
"no-use-before-define": ["error", "nofunc"],
"prefer-destructuring": "off",
"prefer-template": "off",
"no-restricted-properties": "off",
"no-restricted-globals": "off",
"no-multi-assign": "off",
"no-lonely-if": "off",
"consistent-return": "off",
"no-control-regex": "off",
"no-multiple-empty-lines": "warn",
"no-only-tests/no-only-tests": "error",
"operator-linebreak": "off",
"react/destructuring-assignment": "off",
"react/jsx-filename-extension": "off",
"react/jsx-one-expression-per-line": "off",
"react/jsx-uses-react": "error",
"react/jsx-uses-vars": "error",
"react/jsx-wrap-multilines": "warn",
"react/no-access-state-in-setstate": "warn",
"react/prefer-stateless-function": "warn",
"react/forbid-prop-types": "warn",
"react/prop-types": "warn",
"jsx-a11y/anchor-is-valid": "off",
"jsx-a11y/click-events-have-key-events": "off",
"jsx-a11y/label-has-associated-control": [
"warn",
{
controlComponents: true
}
],
"jsx-a11y/label-has-for": "off",
"jsx-a11y/no-static-element-interactions": "off",
"max-len": [
"error",
120,
2,
{
ignoreUrls: true,
ignoreComments: false,
ignoreRegExpLiterals: true,
ignoreStrings: true,
ignoreTemplateLiterals: true
}
],
"no-else-return": ["error", { allowElseIf: true }],
"object-curly-newline": ["error", { consistent: true }]
}
};

View File

@@ -7,4 +7,4 @@ module.exports = {
rules: {
"jest/no-focused-tests": "off",
},
};
};

View File

@@ -1,4 +1,4 @@
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import { configure } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
configure({ adapter: new Adapter() });

View File

@@ -1,5 +1,5 @@
import MockDate from 'mockdate';
import MockDate from "mockdate";
const date = new Date('2000-01-01T02:00:00.000');
const date = new Date("2000-01-01T02:00:00.000");
MockDate.set(date);

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@@ -86,6 +86,11 @@
// Button overrides
.@{btn-prefix-cls} {
transition-duration: 150ms;
&.icon-button {
width: 32px;
padding: 0 10px;
}
}
// Fix ant input number showing duplicate arrows
@@ -374,4 +379,24 @@
line-height: 20px;
margin-top: 9px;
}
}
}
.@{menu-prefix-cls} {
// invert stripe position with class .invert-stripe-position
&-inline.invert-stripe-position {
.@{menu-prefix-cls}-item {
&::after {
right: auto;
left: 0;
}
}
}
}
// overrides for checkbox
@checkbox-prefix-cls: ~'@{ant-prefix}-checkbox';
.@{checkbox-prefix-cls}-wrapper + span,
.@{checkbox-prefix-cls} + span {
padding-right: 0;
}

View File

@@ -1,7 +1,25 @@
.ace_editor {
border: 1px solid #eee;
border: 1px solid fade(@redash-gray, 15%);
height: 100%;
margin-bottom: 10px;
&.ace_autocomplete .ace_completion-highlight {
text-shadow: none !important;
background: #ffff005e;
font-weight: 600;
}
&.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;
}
}
}

View File

@@ -1,8 +0,0 @@
a[ng-click] {
cursor: pointer;
}
/* Immediately apply ng-cloak, instead of waiting for angular.js to load: */
[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
display: none !important;
}

View File

@@ -1,283 +1,273 @@
*, button, input, i, a {
-webkit-font-smoothing: antialiased;
*,
button,
input,
i,
a {
-webkit-font-smoothing: antialiased;
}
*,
*:active,
*:hover {
outline: none !important;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0) !important;
outline: none !important;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0) !important;
}
html {
overflow-x: ~"hidden\0/";
-ms-overflow-style: auto;
overflow-x: ~"hidden\0/";
-ms-overflow-style: auto;
}
html, body {
min-height: 100vh;
html,
body {
min-height: 100vh;
}
body {
padding-top: 0;
background: #F6F8F9;
font-family: @redash-font;
position: relative;
padding-top: 0;
background: #f6f8f9;
font-family: @redash-font;
position: relative;
app-view {
padding-bottom: 15px;
#application-root {
padding-bottom: 15px;
}
&.headless {
#application-root {
padding-top: 10px;
padding-bottom: 0;
}
&.headless {
app-view {
padding-top: 10px;
padding-bottom: 0;
}
.app-header-wrapper {
display: none;
}
.app-header-wrapper {
display: none;
}
}
}
app-view {
min-height: 100vh;
#application-root {
min-height: 100vh;
}
app-view, #app-content {
display: flex;
flex-direction: column;
flex-grow: 1;
#application-root,
#app-content {
display: flex;
flex-direction: column;
flex-grow: 1;
}
strong {
font-weight: 500;
font-weight: 500;
}
#content {
position: relative;
padding-top: 30px;
padding-bottom: 30px;
position: relative;
padding-top: 30px;
padding-bottom: 30px;
@media (min-width: (@screen-sm-min + 1)) {
padding-right: 15px;
padding-left: 15px;
}
@media (min-width: (@screen-sm-min + 1)) {
padding-right: 15px;
padding-left: 15px;
}
@media (min-width: (@screen-lg-min + 80px)) {
margin-left: @sidebar-left-width;
}
@media (min-width: (@screen-lg-min + 80px)) {
margin-left: @sidebar-left-width;
}
@media (min-width: @screen-sm-min) and (max-width: (@screen-md-max + 80px)) {
margin-left: @sidebar-left-mid-width;
}
@media (min-width: @screen-sm-min) and (max-width: (@screen-md-max + 80px)) {
margin-left: @sidebar-left-mid-width;
}
@media (max-width: (@screen-sm-min)) {
margin-left: 0;
}
@media (max-width: (@screen-sm-min)) {
margin-left: 0;
}
}
.container {
&.c-boxed {
max-width: @boxed-width;
}
&.c-boxed {
max-width: @boxed-width;
}
}
// 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;
}
.settings-screen,
.home-page,
.page-dashboard-list,
.page-queries-list,
.page-alerts-list,
.alert-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;
}
.settings-screen,
.home-page,
.page-dashboard-list,
.page-queries-list,
.page-alerts-list,
.alert-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;
}
.settings-screen,
.home-page,
.page-dashboard-list,
.page-queries-list,
.page-alerts-list,
.alert-page,
.fixed-container {
.container {
width: 1170px;
}
}
}
.scrollbox {
overflow: auto;
position: relative;
overflow: auto;
position: relative;
}
.clickable {
cursor: pointer;
cursor: pointer;
}
.resize-vertical {
resize: vertical !important;
transition: height 0s !important;
resize: vertical !important;
transition: height 0s !important;
}
.resize-horizontal {
resize: horizontal !important;
transition: width 0s !important;
resize: horizontal !important;
transition: width 0s !important;
}
.resize-both,
.resize-vertical.resize-horizontal {
resize: both !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;
}
resize: both !important;
transition: height 0s, width 0s !important;
}
.bg-ace {
background-color: fade(@redash-gray, 12%) !important;
background-color: fade(@redash-gray, 12%) !important;
}
// resizeable
.rg-top span, .rg-bottom span {
height: 3px;
border-color: #b1c1ce; // TODO: variable
.rg-top span,
.rg-bottom span {
height: 3px;
border-color: #b1c1ce; // TODO: variable
}
.rg-bottom {
bottom: 15px;
bottom: 15px;
span {
margin: 1.5px 0 0 -10px;
}
span {
margin: 1.5px 0 0 -10px;
}
}
// Plotly
text.slicetext {
text-shadow: 1px 1px 5px #333;
text-shadow: 1px 1px 5px #333;
}
// markdown
.markdown strong {
font-weight: bold;
font-weight: bold;
}
.markdown img {
max-width: 100%;
max-width: 100%;
}
.dropdown-menu > li > a:hover, .dropdown-menu > li > 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;
}
.profile__image--sidebar {
border-radius: 100%;
margin-right: 3px;
margin-top: -2px;
border-radius: 100%;
margin-right: 3px;
margin-top: -2px;
}
.profile__image--settings {
border-radius: 100%;
border-radius: 100%;
}
.profile__image_thumb {
border-radius: 100%;
margin-right: 3px;
margin-top: -2px;
width: 20px;
height: 20px;
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;
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;
}
.error-state__icon {
.zmdi {
font-size: 64px;
color: @redash-gray;
}
}
@media (max-width: 767px) {
margin-top: 10vh;
}
@media (max-width: 767px) {
margin-top: 10vh;
}
}
.warning-icon-danger {
color: @red !important;
color: @red !important;
}
// page
.page-header--new .btn-favourite, .page-header--new .btn-archive {
.page-title {
display: flex;
align-items: center;
h3 {
margin-right: 5px !important;
}
.label {
margin-top: 3px;
display: inline-block;
}
.favorites-control {
font-size: 19px;
margin-right: 5px;
}
}
.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;
}
}
.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;
}
.select-option-divider {
margin: 10px 0 !important;
}

View File

@@ -7,6 +7,7 @@
}
.edit-in-place span.editable {
display: inline-block;
cursor: pointer;
}
@@ -23,32 +24,3 @@
.edit-in-place {
display: inline-block;
}
.edit-in-place {
.rd-form-control {
padding: 0px 6px;
width: 30vw;
}
&.active {
textarea.rd-form-control {
height: 29px;
width: 40vw;
}
}
}
@media (max-width: 880px) {
.edit-in-place {
.rd-form-control {
width: 50vw;
}
&.active {
textarea.rd-form-control {
width: 50vw;
}
}
}
}

View File

@@ -1,9 +1,9 @@
label {
font-weight: 500;
font-weight: 500;
}
textarea.v-resizable {
resize: vertical;
resize: vertical;
}
.form-group {
@@ -29,285 +29,266 @@ textarea.v-resizable {
}
}
/* light version of bootstrap's form-control */
.rd-form-control {
display: block;
padding: 6px 12px;
line-height: 1.428571429;
color: #555555;
vertical-align: middle;
background-color: #ffffff;
border: 1px solid #cccccc;
border-radius: 4px;
-webkit-box-shadow: none;
box-shadow: none;
-webkit-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
width: 90%;
}
/* --------------------------------------------------------
Input Fields
-----------------------------------------------------------*/
.form-control {
.transition(all);
.transition-duration(300ms);
resize: none;
box-shadow: 0 0 0 40px rgba(0, 0, 0, 0) !important;
border-radius: @redash-input-radius;
.transition(all);
.transition-duration(300ms);
resize: none;
box-shadow: 0 0 0 40px rgba(0, 0, 0, 0) !important;
border-radius: @redash-input-radius;
&:focus {
box-shadow: none !important;
border-color: @blue;
}
&:hover {
border-color: @blue;
}
&:focus {
box-shadow: none !important;
border-color: @blue;
}
&:hover {
border-color: @blue;
}
}
/* --------------------------------------------------------
Custom Checkbox + Radio
-----------------------------------------------------------*/
.cra-validatation(@color) {
input[type="checkbox"], input[type="radio"] {
& + .input-helper {
border-color: @color;
}
&:checked + .input-helper:before {
background: @color;
}
input[type="checkbox"],
input[type="radio"] {
& + .input-helper {
border-color: @color;
}
&:checked + .input-helper:before {
background: @color;
}
}
}
.cr-alt {
position: relative;
padding-top: 0;
margin: 0;
label {
position: relative;
padding-top: 0;
padding-left: 28px;
}
&.has-success {
.cra-validatation(@green);
}
&.has-warning {
.cra-validatation(@orange);
}
&.has-error {
.cra-validatation(@red);
}
input[type="checkbox"],
input[type="radio"] {
.opacity(0);
width: 20px;
height: 20px;
position: absolute;
z-index: 10;
margin: 0;
top: 0;
left: 0;
cursor: pointer;
label {
position: relative;
padding-left: 28px;
& + .input-helper {
border: 1px solid @input-border;
width: 19px;
height: 19px;
background: #fff;
position: absolute;
left: 0;
top: -1px;
cursor: pointer;
}
&.has-success {
.cra-validatation(@green);
&:checked + .input-helper:before {
content: "";
width: 9px;
height: 9px;
background: #31acff;
position: absolute;
left: 4px;
top: 4px;
}
}
input[type="radio"] {
& + i {
border-radius: 50%;
}
&.has-warning {
.cra-validatation(@orange);
&:checked + i:before {
border-radius: 50%;
}
}
&.has-error {
.cra-validatation(@red);
}
input[type="checkbox"], input[type="radio"] {
.opacity(0);
width: 20px;
height: 20px;
position: absolute;
z-index: 10;
margin: 0;
top: 0;
left: 0;
cursor: pointer;
& + .input-helper {
border: 1px solid @input-border;
width: 19px;
height: 19px;
background: #fff;
position: absolute;
left: 0;
top: -1px;
cursor: pointer;
}
&:checked + .input-helper:before {
content: "";
width: 9px;
height: 9px;
background: #31ACFF;
position: absolute;
left: 4px;
top: 4px;
}
}
input[type="radio"] {
& + i {
border-radius: 50%;
}
&:checked + i:before {
border-radius: 50%;
}
}
&.disabled {
.opacity(0.7);
}
&.disabled {
.opacity(0.7);
}
}
.checkbox-inline,
.radio-inline {
padding-left: 27px;
padding-left: 27px;
}
/* --------------------------------------------------------
Input Addon
-----------------------------------------------------------*/
.input-group {
.input-group-addon {
min-width: 40px;
color: #333;
padding: 0;
}
&:not([class*="input-group-"]) {
.input-group-addon {
min-width: 40px;
color: #333;
padding: 0;
}
&:not([class*="input-group-"]) {
.input-group-addon {
font-size: 15px;
}
font-size: 15px;
}
}
}
/* --------------------------------------------------------
Toggle Switch
-----------------------------------------------------------*/
.ts-color(@color){
input {
&:not(:disabled) {
&:checked {
& + .ts-helper {
background: fade(@color, 50%);
.ts-color(@color) {
input {
&:not(:disabled) {
&:checked {
& + .ts-helper {
background: fade(@color, 50%);
&:before {
background: @color;
}
&:before {
background: @color;
}
&:active {
&:before {
box-shadow: 0 2px 8px rgba(0,0,0,0.28), 0 0 0 20px fade(@color, 20%);
}
}
}
&:active {
&:before {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.28), 0 0 0 20px fade(@color, 20%);
}
}
}
}
}
}
}
.toggle-switch {
display: inline-block;
vertical-align: top;
.user-select(none);
.ts-label {
display: inline-block;
margin: 0 20px 0 0;
vertical-align: top;
.user-select(none);
-webkit-transition: color 0.56s cubic-bezier(0.4, 0, 0.2, 1);
transition: color 0.56s cubic-bezier(0.4, 0, 0.2, 1);
}
.ts-label {
display: inline-block;
margin: 0 20px 0 0;
vertical-align: top;
-webkit-transition: color 0.56s cubic-bezier(0.4, 0, 0.2, 1);
transition: color 0.56s cubic-bezier(0.4, 0, 0.2, 1);
.ts-helper {
display: inline-block;
position: relative;
width: 40px;
height: 16px;
border-radius: 8px;
background: rgba(0, 0, 0, 0.26);
-webkit-transition: background 0.28s cubic-bezier(0.4, 0, 0.2, 1);
transition: background 0.28s cubic-bezier(0.4, 0, 0.2, 1);
vertical-align: middle;
cursor: pointer;
&:before {
content: "";
position: absolute;
top: -4px;
left: -4px;
width: 24px;
height: 24px;
background: #fafafa;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.28);
border-radius: 50%;
webkit-transition: left 0.28s cubic-bezier(0.4, 0, 0.2, 1), background 0.28s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.28s cubic-bezier(0.4, 0, 0.2, 1);
transition: left 0.28s cubic-bezier(0.4, 0, 0.2, 1), background 0.28s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.28s cubic-bezier(0.4, 0, 0.2, 1);
}
}
&:not(.disabled) {
.ts-helper {
display: inline-block;
position: relative;
width: 40px;
height: 16px;
border-radius: 8px;
background: rgba(0,0,0,0.26);
-webkit-transition: background 0.28s cubic-bezier(0.4, 0, 0.2, 1);
transition: background 0.28s cubic-bezier(0.4, 0, 0.2, 1);
vertical-align: middle;
cursor: pointer;
&:active {
&:before {
content: '';
position: absolute;
top: -4px;
left: -4px;
width: 24px;
height: 24px;
background: #fafafa;
box-shadow: 0 2px 8px rgba(0,0,0,0.28);
border-radius: 50%;
webkit-transition: left 0.28s cubic-bezier(0.4, 0, 0.2, 1), background 0.28s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.28s cubic-bezier(0.4, 0, 0.2, 1);
transition: left 0.28s cubic-bezier(0.4, 0, 0.2, 1), background 0.28s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.28s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.28), 0 0 0 20px rgba(128, 128, 128, 0.1);
}
}
}
}
&:not(.disabled) {
.ts-helper {
&:active {
&:before {
box-shadow: 0 2px 8px rgba(0,0,0,0.28), 0 0 0 20px rgba(128,128,128,0.1);
}
}
input {
position: absolute;
z-index: 1;
width: 46px;
margin: 0 0 0 -4px;
height: 24px;
.opacity(0);
cursor: pointer;
&:checked {
& + .ts-helper {
&:before {
left: 20px;
}
}
}
}
input {
position: absolute;
z-index: 1;
width: 46px;
margin: 0 0 0 -4px;
height: 24px;
.opacity(0);
cursor: pointer;
&:not([data-ts-color]) {
.ts-color(@teal);
}
&:checked {
& + .ts-helper {
&:before {
left: 20px;
}
}
}
}
&.disabled {
.opacity(0.6);
}
&:not([data-ts-color]){
.ts-color(@teal);
}
&[data-ts-color="red"] {
.ts-color(@red);
}
&.disabled {
.opacity(0.6);
}
&[data-ts-color="blue"] {
.ts-color(@blue);
}
&[data-ts-color="red"] {
.ts-color(@red);
}
&[data-ts-color="amber"] {
.ts-color(@amber);
}
&[data-ts-color="blue"] {
.ts-color(@blue);
}
&[data-ts-color="purple"] {
.ts-color(@purple);
}
&[data-ts-color="amber"] {
.ts-color(@amber);
}
&[data-ts-color="pink"] {
.ts-color(@pink);
}
&[data-ts-color="purple"] {
.ts-color(@purple);
}
&[data-ts-color="lime"] {
.ts-color(@lime);
}
&[data-ts-color="pink"] {
.ts-color(@pink);
}
&[data-ts-color="lime"] {
.ts-color(@lime);
}
&[data-ts-color="cyan"] {
.ts-color(@cyan);
}
&[data-ts-color="green"] {
.ts-color(@green);
}
&[data-ts-color="cyan"] {
.ts-color(@cyan);
}
&[data-ts-color="green"] {
.ts-color(@green);
}
}

View File

@@ -1,28 +0,0 @@
/* angular-growl */
.growl {
position: fixed;
bottom: 10px;
right: 10px;
float: right;
width: 250px;
z-index: 10000;
}
.growl-item.ng-enter,
.growl-item.ng-leave {
-webkit-transition: 0.5s linear all;
-moz-transition: 0.5s linear all;
-o-transition: 0.5s linear all;
transition: 0.5s linear all;
}
.growl-item.ng-enter,
.growl-item.ng-leave.ng-leave-active {
opacity: 0;
}
.growl-item.ng-leave,
.growl-item.ng-enter.ng-enter-active {
opacity: 1;
}

View File

@@ -17,31 +17,6 @@
}
}
tags-list {
a {
line-height: 1.1;
}
}
.tags-list__name {
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
width: 88%;
line-height: 1.3;
}
.tags-list {
.badge-light {
background: fade(@redash-gray, 10%);
color: fade(@redash-gray, 75%);
}
a:hover {
cursor: pointer;
}
}
.max-character {
.text-overflow();
}

View File

@@ -234,4 +234,9 @@
.hide-in-percy, .pace {
visibility: hidden;
}
// hide tooltips in Percy
.ant-tooltip {
display: none !important;
}
}

View File

@@ -1,11 +0,0 @@
.overlay {
background-color: #808080;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
padding: 0;
z-index: 1000;
opacity: 0.8;
}

View File

@@ -31,6 +31,8 @@ div.table-name {
overflow-x: hidden;
border: none;
margin-top: 10px;
position: relative;
height: 100%;
.collapse.in {
background: transparent;
@@ -72,11 +74,12 @@ div.table-name {
.schema-control {
display: flex;
flex-wrap: nowrap;
padding: 0;
.form-control {
margin-right: 5px;
}
.ant-btn {
height: auto;
}
}
.parameter-label {

View File

@@ -1,146 +0,0 @@
.tab-nav {
list-style: none;
padding: 0;
white-space: nowrap;
margin: 0 0 10px 0;
overflow: auto;
box-shadow: inset 0 -2px 0 0 #eee;
& > li {
display: inline-block;
vertical-align: top;
& > a {
display: inline-block;
color: #7a7a7a;
text-transform: uppercase;
position: relative;
width: 100%;
font-weight: 500;
&:after {
content: "";
height: 2px;
position: absolute;
width: 100%;
left: 0;
bottom: 0;
display: none;
}
@media (min-width: @screen-sm-min) {
padding: 15px;
}
@media (max-width: @screen-sm-min) {
padding: 15px 8px;
}
}
&.active {
& > a {
color: #000;
&:after {
display: block;
}
}
}
}
&.tab-nav-right {
text-align: right;
}
&.tn-justified {
& > li {
display: table-cell;
width: 1%;
text-align: center;
}
}
&.tn-icon {
& > li {
.zmdi {
font-size: 22px;
line-height: 100%;
min-height: 25px;
}
}
}
&:not([data-tab-color]) {
& > li > a:after {
background: @blue;
}
}
&[data-tab-color="green"] {
& > li > a:after {
background: @green;
}
}
&[data-tab-color="red"] {
& > li > a:after {
background: @red;
}
}
&[data-tab-color="teal"] {
& > li > a:after {
background: @teal;
}
}
&[data-tab-color="amber"] {
& > li > a:after {
background: @amber;
}
}
&[data-tab-color="black"] {
& > li > a:after {
background: @black;
}
}
&[data-tab-color="cyan"] {
& > li > a:after {
background: @cyan;
}
}
}
.tab-content {
padding: 20px 0;
}
.rd-tab {
.remove {
cursor: pointer;
color: #A09797;
padding: 0 3px 1px 4px;
font-size: 11px;
&:hover {
color: white;
background-color: #FF8080;
border-radius: 50%;
}
}
}
.tab-nav {
margin-bottom: 0px;
> li.rd-tab-btn {
float: right;
padding-right: 10px;
padding-top: 10px;
}
> li > a {
text-transform: capitalize;
}
}

View File

@@ -1,29 +0,0 @@
#toast-container .toast {
margin: 0 6px 6px 0;
box-shadow: none;
color: #ffffff;
opacity: 0.75;
border-radius: 2px;
transition: opacity 0.35s ease-in-out;
}
#toast-container .toast:hover {
box-shadow: none;
opacity: 1;
cursor: pointer;
}
.toast {
background-color: #030303;
}
.toast-success {
background-color: #3BD973;
}
.toast-error {
background-color: #E92828;
}
.toast-info {
background-color: #356AFF;
}
.toast-warning {
background-color: #FB8D3D;
}

View File

@@ -1,125 +0,0 @@
.bootgrid-table {
margin: 0;
box-shadow: none;
}
.bootgrid-footer .infoBar,
.bootgrid-header .actionBar {
text-align: left;
}
.bootgrid-footer .search,
.bootgrid-header .search {
vertical-align: top;
}
.bootgrid-header {
margin: 0;
padding: 25px;
.search {
border: 1px solid @input-border;
.form-control, .input-group-addon {
border: 0;
}
.input-group-addon {
font-size: 18px;
color: #333;
padding-right: 0 !important;
min-width: 26px;
text-align: right;
}
@media (min-width: @screen-xs-min) {
width: 300px;
}
@media (max-width: @screen-xs-min) {
width: 100%;
padding-right: 90px;
}
}
.actions {
box-shadow: none;
.btn-group {
.btn {
height: 37px;
background: #fff;
border-radius: 0;
border: 1px solid @input-border;
}
.dropdown-menu {
@media (min-width: @screen-sm-min) {
left: 0;
margin-top: 1px;
}
.dropdown-item {
padding: 5px 10px;
.input-helper {
top: 5px;
}
}
}
.caret {
display: none;
}
.zmdi {
line-height: 100%;
font-size: 18px;
vertical-align: top;
}
}
@media (max-width: @screen-xs-min) {
position: absolute;
top: 0;
right: 15px;
}
}
}
.bootgrid-footer {
border-top: 1px solid @table-border-color;
margin-top: 0;
.col-sm-6 {
padding: 25px;
@media (max-width: @screen-sm-min) {
text-align: center;
}
}
.infoBar {
@media (max-width: @screen-sm-min) {
display: none;
}
.infos {
border: 1px solid #EEE;
display: inline-block;
float: right;
padding: 7px 30px;
font-size: 12px;
margin-top: 3px;
}
}
}
.select-cell .checkbox {
margin: 0px 0 0 -19px;
top: 3px;
}

View File

@@ -1,215 +0,0 @@
.bootstrap-datetimepicker-widget {
padding: 0 !important;
margin: 0 !important;
width: auto !important;
&:after, &:before { display: none !important; }
table td {
text-shadow: none;
span {
margin: 0;
&:hover { background: transparent; }
}
}
.glyphicon { font-family: @font-icon; font-size: 18px; }
.glyphicon-chevron-left:before { content: "\f2ff"; }
.glyphicon-chevron-right:before { content: "\f301"; }
.glyphicon-time:before { content: "\f337"; }
.glyphicon-calendar:before { content: "\f32e"; }
.glyphicon-chevron-up:before { content: "\f1e5"; }
.glyphicon-chevron-down:before { content: "\f1e4"; }
[data-action="togglePicker"] span {
font-size: 25px;
color: #ccc;
&:hover {
color: #333;
}
}
a[data-action] {
color: @blue;
}
}
.timepicker-picker {
.btn { box-shadow: none !important; }
table {
tbody tr + tr:not(:last-child) {
background: @blue;
color: #fff;
td {
border-radius: 0;
}
}
}
.btn {
background: #fff;
color: #333;
}
}
.datepicker {
&.top {
.transform-origin(0 100%) !important;
}
table {
thead {
tr {
th {
border-radius: 0;
color: #fff;
.glyphicon {
width: 30px;
height: 30px;
border-radius: 50%;
line-height: 29px;
}
&:hover .glyphicon {
background: rgba(0, 0, 0, 0.2);
}
}
&:first-child {
th {
background: @blue;
padding: 20px 0;
&:hover {
background: @blue;
}
&.picker-switch {
font-size: 16px;
font-weight: 400;
text-transform: uppercase;
}
}
}
&:last-child {
th {
&:first-child { padding-left: 20px; }
&:last-child { padding-right: 20px; }
text-transform: uppercase;
font-weight: normal;
font-size: 11px;
}
&:not(:only-child) {
background: darken(@blue, 3%);
}
}
}
}
tbody {
tr {
&:last-child {
td {
padding-bottom: 25px;
}
}
td {
&:first-child {
padding-left: 13px;
}
&:last-child {
padding-right: 13px;
}
}
}
}
td {
&.day {
width: 35px;
height: 35px;
line-height: 20px;
color: #333;
position: relative;
padding: 0;
background: transparent;
&:hover {
background: none;
}
&:before {
content: "";
width: 35px;
height: 35px;
border-radius: 50%;
margin-bottom: -33px;
display: inline-block;
background: transparent;
position: static;
text-shadow: none;
}
&.old, &.new {
color: #CDCDCD;
}
}
&:not(.today):not(.active) {
&:hover:before {
background: #F0F0F0;
}
}
&.today {
color: #333;
&:before {
background-color: #E2E2E2;
}
}
&.active {
color: #fff;
&:before {
background-color: @blue;
}
}
}
}
}
.datepicker-months .month,
.datepicker-years .year,
.timepicker-minutes .minute,
.timepicker-hours .hour {
border-radius: 50%;
&:not(.active) {
&:hover {
background: #F0F0F0;
}
}
&.active {
background: @blue;
}
}
.timepicker-minutes .minute,
.timepicker-hours .hour {
padding: 0;
}

View File

@@ -1,72 +0,0 @@
.bootstrap-select {
.bs-searchbox {
padding: 0 18px;
margin: 5px 0 10px;
position: relative;
&:before {
position: absolute;
left: 14px;
top: 2px;
width: 30px;
height: 100%;
content: "\f1c3";
font-family: @font-icon;
font-size: 25px;
}
input {
padding-left: 25px;
border: 0;
}
}
&.btn-group {
.dropdown-menu li a.opt {
padding-left: 17px;
}
}
.check-mark {
margin-top: -5px !important;
font-size: 19px;
display: none;
position: absolute;
top: 11px;
right: 15px;
&:before {
content: "\f26b";
font-family: @font-icon;
}
}
.selected {
.check-mark {
display: block !important;
}
}
.notify {
bottom: 0 !important;
margin: 0 !important;
width: 100% !important;
border: 0 !important;
background: @red !important;
color: #fff !important;
text-align: center;
}
&:not([class*=col-]):not([class*=form-control]):not(.input-group-btn) {
width: 100%;
}
.btn-default {
background-color: #fff;
border-radius: 0;
border: 1px solid @input-border;
}
}

View File

@@ -1,114 +0,0 @@
.chosen-container {
.chosen-drop {
border-color: @input-border;
border-radius: 0;
}
.chosen-results {
margin: 10px 0 0 0;
padding: 0;
li {
padding: 10px 17px;
width: 100%;
&.highlighted {
background: @dropdown-link-hover-bg;
color: @dropdown-link-hover-color;
}
&.result-selected {
background: @lightblue;
color: @white;
position: relative;
&:before {
content: "\f26b";
font-family: @font-icon;
position: absolute;
right: 15px;
top: 10px;
font-size: 19px;
}
}
&.group-result {
&:not(:first-child) {
border-top: 1px solid #eee;
}
color: #B2B2B2;
font-weight: normal;
padding: 16px 15px 6px;
margin-top: 9px;
}
}
}
}
.chosen-container-single {
.chosen-single {
border-radius: 0;
height: 35px;
padding: 7px 12px 6px;
line-height: 1.42857143;
border-color: @input-border;
}
.chosen-search {
padding: 5px 12px;
&:before {
content: "\f1c3";
font-family: @font-icon;
position: absolute;
left: 25px;
top: 9px;
font-size: 19px;
}
input[type=text] {
border-color: @input-border;
padding: 8px 10px 8px 35px;
}
}
}
.chosen-container-multi {
.chosen-choices {
padding: 0 4px;
border-color: @input-border;
li {
&.search-choice {
border-radius: 0;
margin: 4px 4px 0 0;
background: @blue;
border-color: @blue;
color: #fff;
padding: 5px 23px 5px 8px;
.search-choice-close {
&:before {
display: inline-block;
font-family: @font-icon;
content: "\f135";
position: relative;
top: 1px;
color: #fff;
z-index: 2;
font-size: 12px;
}
}
}
&.search-field {
input[type=text] {
padding: 0 8px;
height: 31px;
}
}
}
}
}

View File

@@ -1,25 +0,0 @@
.cp-container {
position: relative;
& > .input-group {
input.cp-value {
color: #000 !important;
background: transparent !important;
}
.dropdown-menu {
padding: 20px;
margin-top: 30px;
}
}
i.cp-value {
width: 25px;
height: 25px;
border-radius: 50%;
position: absolute;
top: 5px;
right: 5px;
}
}

View File

@@ -1,51 +0,0 @@
.fileinput {
position: relative;
padding-right: 35px;
.close {
position: absolute;
top: 5px;
font-size: 12px;
float: none;
opacity: 1;
font-weight: 500;
border: 1px solid #ccc;
width: 19px;
text-align: center;
height: 19px;
line-height: 15px;
border-radius: 50%;
right: 0;
&:hover {
background: #eee;
}
}
.btn-file {
}
.input-group-addon {
padding: 0 10px;
vertical-align: middle;
}
.fileinput-preview {
width: 200px;
height: 150px;
position: relative;
img {
display: inline-block;
vertical-align: middle;
margin-top: -13px;
}
&:after {
content: "";
display: inline-block;
vertical-align: middle;
}
}
}

View File

@@ -1,207 +0,0 @@
/** CALENDAR WIDGET **/
#calendar-widget {
margin-bottom: 30px;
box-shadow: 0 1px 1px rgba(0,0,0,.15);
}
#fc-actions {
position: absolute;
bottom: 23px;
right: 22px;
& > li > a {
font-size: 20px;
color: #fff;
width: 30px;
height: 30px;
border-radius: 50%;
line-height: 30px;
}
& > li.open > a,
& > li > a:hover {
background: darken(@teal, 7%);
}
}
.fc {
background-color: #fff;
margin-bottom: 20px;
td {
border-color: @table-border-color !important;
}
th {
background: darken(@teal, 7%);
color: #fff;
font-weight: 400;
padding: 6px 0;
}
table tr {
& > td:first-child {
border-left-width: 0;
}
}
.ui-widget-header {
border-width: 0;
}
.fc-day-number {
color: #CCC;
}
.fc-event-container {
padding: 0 2px 2px;
}
}
.fc-toolbar {
background: @teal;
margin-bottom: 0;
padding: 25px 7px 25px;
position: relative;
.user-select(none);
&:before {
content: "";
bottom: -30px;
height: 30px;
width: 100%;
background: darken(@teal, 7%);
position: absolute;
left: 0;
z-index: 0;
}
h2 {
color: rgba(255, 255, 255, 0.9);
margin-top: 7px;
font-size: 19px;
font-weight: 400;
}
.ui-button {
border: 0;
background: 0 0;
padding: 0;
outline: none !important;
text-align: center;
& > span {
position: relative;
font-family: @font-icon;
font-size: 20px;
color: #FFF;
line-height: 100%;
width: 31px;
height: 31px;
border-radius: 50%;
padding-top: 6px;
display: block;
margin-top: 2px;
&:before {
position: relative;
z-index: 1;
}
&.ui-icon-circle-triangle-w:before {
content: "\f2fa";
}
&.ui-icon-circle-triangle-e:before {
content: "\f2fb";
}
&:hover {
background: darken(@teal, 7%);
color: #fff;
}
}
}
}
.fc-event {
padding: 0;
font-size: 11px;
border-radius: 0;
border: 0;
.fc-title {
padding: 3px 5px 2px;
display: block;
}
.fc-time {
float: left;
background: rgba(0, 0, 0, 0.2);
padding: 2px 6px;
margin: 0 0 0 -1px;
}
}
.fc-view, .fc-view > table {
border: 0;
overflow: hidden;
}
.fc-content-skeleton {
table {
background: transparent;
}
}
#calendar {
.fc-day-number {
@media screen and (min-width: @screen-sm-max) {
font-size: 25px;
letter-spacing: -2px;
}
padding-left: 10px !important;
text-align: left !important;
}
}
/* Even Tag Color */
.event-tag {
margin-top: 5px;
& > span {
border-radius: 50%;
width: 30px;
height: 30px;
margin-right: 3px;
position: relative;
display: inline-block;
cursor: pointer;
&:hover {
.opacity(0.8);
}
&.selected {
&:before {
font-family: @font-icon;
content: "\f26b";
position: absolute;
text-align: center;
top: 3px;
width: 100%;
font-size: 17px;
color: #FFF;
}
}
}
}
/* Height Fix */
.fc-day-grid-container {
height: auto !important;
}

View File

@@ -1,69 +0,0 @@
.lg-outer .lg-item {
background-image: none;
}
.lg-slide {
&:after {
content: "";
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
height: 50px;
width: 50px;
border-radius: 100%;
border: 2px solid @white;
-webkit-animation: ball-scale-ripple 1s 0s infinite cubic-bezier(0.21, 0.53, 0.56, 0.8);
animation: ball-scale-ripple 1s 0s infinite cubic-bezier(0.21, 0.53, 0.56, 0.8);
position: absolute;
left: 50%;
margin-left: -25px;
top: 50%;
margin-top: -25px;
z-index: -1;
}
em {
font-style: normal;
display: block;
margin-bottom: 20px;
h3 {
margin-bottom: 5px;
color: #D2D2D2;
font-weight: 100;
}
p {
color: #6B6B6B;
}
}
}
@-webkit-keyframes ball-scale-ripple {
0% {
-webkit-transform: scale(0.1);
transform: scale(0.1);
opacity: 1; }
70% {
-webkit-transform: scale(1);
transform: scale(1);
opacity: 0.7; }
100% {
opacity: 0.0; }
}
@keyframes ball-scale-ripple {
0% {
-webkit-transform: scale(0.1);
transform: scale(0.1);
opacity: 1; }
70% {
-webkit-transform: scale(1);
transform: scale(1);
opacity: 0.7; }
100% {
opacity: 0.0; }
}

View File

@@ -1,25 +0,0 @@
.mCSB_container,
.mCustomScrollBox {
overflow: visible;
}
.mCSB_scrollTools {
width: 12px;
.mCSB_draggerRail,
.mCSB_dragger .mCSB_dragger_bar {
border-radius: 0;
}
.mCSB_draggerRail {
background: transparent !important;
}
}
.mCS-dark.mCSB_scrollTools .mCSB_dragger .mCSB_dragger_bar {
background: rgba(0,0,0,0.4);
}
.mCSB_inside > .mCSB_container {
margin-right: 0;
}

View File

@@ -1,172 +0,0 @@
.noUi-target {
border-radius: 0;
box-shadow: none;
border: 0;
}
.noUi-background {
background: #d4d4d4;
box-shadow: none;
}
.noUi-horizontal {
height: 3px;
.noUi-handle {
top: -8px;
}
}
.noUi-vertical {
width: 3px;
}
.noUi-horizontal,
.noUi-vertical {
.noUi-handle {
width: 19px;
height: 19px;
border: 0;
border-radius: 100%;
box-shadow: none;
.transition(box-shadow);
.transition-duration(200ms);
cursor: pointer;
position: relative;
&:before,
&:after {
display: none;
}
&:active {
background: #ccc !important;
}
.is-tooltip {
position: absolute;
bottom: 32px;
height: 35px;
border-radius: 2px;
color: #fff;
text-align: center;
line-height: 33px;
width: 50px;
left: 50%;
margin-left: -25px;
padding: 0 10px;
.transition(all);
.transition-duration(200ms);
.backface-visibility(hidden);
.opacity(0);
.scale(0);
&:after {
width: 0;
height: 0;
border-style: solid;
border-width: 15px 10px 0 10px;
position: absolute;
bottom: -8px;
left: 50%;
margin-left: -9px;
content: "";
}
}
}
.noUi-active {
box-shadow: 0 0 0 13px rgba(0,0,0,0.1);
.is-tooltip {
.scale(1);
bottom: 40px;
.opacity(1);
}
}
}
.input-slider,
.input-slider-range,
.input-slider-values {
&:not([data-is-color]) {
.noUi-handle,
.noUi-connect, {
background: @teal !important;
}
.is-tooltip {
background: @teal;
&:after {
border-color: @teal transparent transparent transparent;
}
}
}
&[data-is-color=red] {
.is-color-handle(@red);
}
&[data-is-color=blue] {
.is-color-handle(@blue);
}
&[data-is-color=cyan] {
.is-color-handle(@cyan);
}
&[data-is-color=amber] {
.is-color-handle(@amber);
}
&[data-is-color=green] {
.is-color-handle(@green);
}
}
.input-slider {
.noUi-origin {
background: #d4d4d4;
}
&:not([data-is-color]) {
.noUi-base {
background: @teal !important;
}
}
&[data-is-color=red] {
.is-color-base(@red);
}
&[data-is-color=blue] {
.is-color-base(@blue);
}
&[data-is-color=cyan] {
.is-color-base(@cyan);
}
&[data-is-color=amber] {
.is-color-base(@amber);
}
&[data-is-color=green] {
.is-color-base(@green);
}
}
.is-color-handle(@color) {
.noUi-handle,
.noUi-connect {
background: @color !important;
}
}
.is-color-base(@color) {
.noUi-base {
background: @color !important;
}
}

View File

@@ -1,194 +0,0 @@
.note-editor,
.note-popover {
.note-toolbar,
.popover-content {
background: #fff;
border-color: #e4e4e4;
margin: 0;
padding: 10px 0 15px;
text-align: center;
& > .btn-group {
display: inline-block;
float: none;
box-shadow: none;
.btn {
margin: 0 1px;
border: 0;
}
& > .active {
background: @cyan;
color: #fff;
}
}
.btn {
height: 40px;
border-radius: 2px !important;
box-shadow: none !important;
background: #fff;
&:active {
box-shadow: none;
}
}
.note-palette-title {
margin: 0 !important;
padding: 10px 0 !important;
font-size: 13px !important;
text-align: center !important;
border: 0 !important;
}
.note-color-reset {
padding: 0 0 10px !important;
margin: 0 !important;
background: none;
text-align: center;
}
.note-color {
.dropdown-menu {
min-width: 335px;
}
}
}
.note-statusbar {
.note-resizebar {
border-color: #E8E8E8;
.note-icon-bar {
border-color: #BCBCBC;
}
}
}
.fa {
font-style: normal;
font-size: 20px;
vertical-align: middle;
&:before {
font-family: @font-icon;
}
&.fa-magic:before {
content: "\f16a";
}
&.fa-bold:before {
content: "\f23d";
}
&.fa-italic:before {
content: "\f245";
}
&.fa-underline:before {
content: "\f24f";
}
&.fa-font:before {
content: "\f242";
}
&.fa-list-ul:before {
content: "\f247";
}
&.fa-list-ol:before {
content: "\f248";
}
&.fa-align-left:before {
content: "\f23b";
}
&.fa-align-right:before {
content: "\f23c";
}
&.fa-align-center:before {
content: "\f239";
}
&.fa-align-justify:before {
content: "\f23a";
}
&.fa-indent:before {
content: "\f244";
}
&.fa-outdent:before {
content: "\f243";
}
&.fa-text-height:before {
content: "\f246";
}
&.fa-table:before {
content: "\f320";
}
&.fa-link:before {
content: "\f18e";
}
&.fa-picture-o:before {
content: "\f17f";
}
&.fa-minus:before {
content: "\f22f";
}
&.fa-arrows-alt:before {
content: "\f16d";
}
&.fa-code:before {
content: "\f13a";
}
&.fa-question:before {
content: "\f1f5";
}
&.fa-eraser:before {
content: "\f23f";
}
&.fa-square:before {
content: "\f279";
}
&.fa-circle-o:before {
content: "\f26c";
}
&.fa-times:before {
content: "\f136";
}
}
.note-air-popover {
.arrow {
left: 20px;
}
}
}
.note-editor {
border: 1px solid #e4e4e4;
.note-editable {
padding: 20px 23px;
}
}

View File

@@ -1,21 +0,0 @@
.sweet-alert {
border-radius: 2px;
padding: 10px 30px;
h2 {
font-size: 16px;
font-weight: 400;
position: relative;
z-index: 1;
}
.lead {
font-size: 13px;
}
.btn {
padding: 6px 12px;
font-size: 13px;
margin: 20px 2px 0;
}
}

View File

@@ -1,24 +0,0 @@
.twitter-typeahead {
width: 100%;
.tt-menu {
min-width: 200px;
background: #fff;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
}
.tt-suggestion:hover,
.tt-cursor {
background-color: rgba(0,0,0,0.075);
}
.tt-suggestion {
padding: 8px 17px;
color: #333;
cursor: pointer;
}
.tt-hint {
color: #818181 !important;
}
}

View File

@@ -1,35 +0,0 @@
/* ui-select adjustments for SuperFlat */
.clearable button {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
/* Same definition as .form-control */
.ui-select-toggle.btn-default {
height: 35px;
padding: 6px 12px;
font-size: 13px;
line-height: 1.42857143;
color: #9E9E9E;
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 2px;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
-webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
-o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
&:hover, &:active, &.active, &:focus, &.focus {
background: #fff;
}
}
.btn-default-focus {
outline: none;
outline-offset: 0;
box-shadow: none;
background: none;
}

View File

@@ -1,28 +0,0 @@
cohort-renderer {
display: block;
}
.cornelius-container {
padding: 0;
margin: 0;
.cornelius-table {
width: 100%;
margin: 0;
box-shadow: none;
border-radius: 0;
background: transparent;
tr, th, td {
border-color: @table-border-color;
}
td {
border-radius: 0 !important;
}
.cornelius-time, .cornelius-label, .cornelius-people {
background-color: fade(@redash-gray, 3%) !important;
}
}
}

View File

@@ -1,15 +0,0 @@
.col-table .missing-value {
color: #b94a48;
}
.col-table .super-small-input {
padding-left: 3px;
height: 24px;
}
.col-table .ui-select-toggle, .col-table .ui-select-search {
padding: 2px;
padding-left: 5px;
height: 24px;
}

View File

@@ -6,32 +6,9 @@
height: 100%;
z-index: 0;
}
.map-custom-control.leaflet-bar {
background: #fff;
padding: 10px;
margin: 10px;
position: absolute;
z-index: 1;
&.top-left {
left: 0;
top: 0;
}
&.top-right {
right: 0;
top: 0;
}
&.bottom-left {
left: 0;
bottom: 0;
}
&.bottom-right {
right: 0;
bottom: 0;
}
}
}
.leaflet-popup-content img {
max-width: 100%;
height: auto;
}

View File

@@ -1,75 +1,57 @@
/** LESS Plugins **/
@import 'inc/less-plugins/for';
@import "inc/less-plugins/for";
/** Load Main Bootstrap LESS files **/
@import '~bootstrap/less/bootstrap';
@import "~bootstrap/less/bootstrap";
/** Load Vendors Dependencies **/
@import '~font-awesome/less/font-awesome';
@import '~ui-select/dist/select.css';
@import '~angular-resizable/src/angular-resizable.css';
@import '~material-design-iconic-font/dist/css/material-design-iconic-font.css';
@import '~pace-progress/themes/blue/pace-theme-minimal.css';
@import "~font-awesome/less/font-awesome";
@import "~material-design-iconic-font/dist/css/material-design-iconic-font.css";
@import "~pace-progress/themes/blue/pace-theme-minimal.css";
@import 'inc/angular';
@import 'inc/variables';
@import 'inc/mixins';
@import 'inc/font';
@import 'inc/print';
@import "inc/variables";
@import "inc/mixins";
@import "inc/font";
@import "inc/print";
@import 'inc/bootstrap-overrides';
@import 'inc/base';
@import 'inc/generics';
@import 'inc/form';
@import 'inc/button';
@import 'inc/list';
@import 'inc/header';
@import 'inc/tile';
@import 'inc/label';
@import 'inc/dropdown';
@import 'inc/list-group';
@import 'inc/misc';
@import 'inc/progress-bar';
@import 'inc/widgets';
@import 'inc/table';
@import 'inc/alert';
@import 'inc/media';
@import 'inc/modal';
@import 'inc/tab';
@import 'inc/panel';
@import 'inc/tooltips';
@import 'inc/popover';
@import 'inc/breadcrumb';
@import 'inc/jumbotron';
@import 'inc/profile';
@import 'inc/404';
@import 'inc/ie-warning';
@import 'inc/edit-in-place';
@import 'inc/growl';
@import 'inc/flex';
@import 'inc/ace-editor';
@import 'inc/overlay';
@import 'inc/schema-browser';
@import 'inc/toast';
@import 'inc/visualizations/box';
@import 'inc/visualizations/pivot-table';
@import 'inc/visualizations/map';
@import 'inc/visualizations/cohort';
@import 'inc/visualizations/misc';
/** VENDOR OVERRIDES **/
@import 'inc/vendor-overrides/bootstrap-select';
@import 'inc/vendor-overrides/bootstrap-datetimepicker';
@import 'inc/vendor-overrides/typeahead';
@import 'inc/vendor-overrides/sweetalert';
@import 'inc/vendor-overrides/ui-select';
@import "inc/bootstrap-overrides";
@import "inc/base";
@import "inc/generics";
@import "inc/form";
@import "inc/button";
@import "inc/list";
@import "inc/header";
@import "inc/tile";
@import "inc/label";
@import "inc/dropdown";
@import "inc/list-group";
@import "inc/misc";
@import "inc/progress-bar";
@import "inc/widgets";
@import "inc/table";
@import "inc/alert";
@import "inc/media";
@import "inc/modal";
@import "inc/panel";
@import "inc/tooltips";
@import "inc/popover";
@import "inc/breadcrumb";
@import "inc/jumbotron";
@import "inc/profile";
@import "inc/404";
@import "inc/ie-warning";
@import "inc/edit-in-place";
@import "inc/flex";
@import "inc/ace-editor";
@import "inc/schema-browser";
@import "inc/visualizations/box";
@import "inc/visualizations/pivot-table";
@import "inc/visualizations/map";
@import "inc/visualizations/misc";
/** REDASH STYLING **/
@import 'redash/redash-table';
@import 'redash/query';
@import 'redash/tags-control';
@import 'redash/css-logo';
@import 'redash/loading-indicator';
@import "redash/redash-table";
@import "redash/query";
@import "redash/tags-control";
@import "redash/css-logo";
@import "redash/loading-indicator";

View File

@@ -39,8 +39,8 @@
}
}
// hide indicator when app-view has content
app-view:not(:empty) ~ .loading-indicator {
// hide indicator when application has content
#application-root:not(:empty) ~ .loading-indicator {
opacity: 0;
transform: scale(0.9);
pointer-events: none;
@@ -48,4 +48,4 @@ app-view:not(:empty) ~ .loading-indicator {
* {
animation: none !important;
}
}
}

View File

@@ -2,7 +2,7 @@ body.fixed-layout {
padding: 0;
overflow: hidden;
app-view {
#application-root {
display: flex;
flex-direction: column;
padding-bottom: 0;
@@ -17,24 +17,15 @@ body.fixed-layout {
}
}
.tab-nav .tab-new-vis {
margin: 0 5px;
> a {
color: @headings-color;
margin-top: 8px;
padding: 7px;
font-weight: 400;
}
}
.bottom-controller {
padding: 10px 15px;
background: #fff;
display: flex;
align-items: center;
button, div, span {
button,
div,
span {
position: relative;
}
@@ -44,7 +35,7 @@ body.fixed-layout {
}
&:before {
content: '';
content: "";
height: 50px;
position: fixed;
bottom: 0;
@@ -55,7 +46,7 @@ body.fixed-layout {
}
.p-b-60 {
padding-bottom: 60px !important;
padding-bottom: 60px !important;
}
.bottom-controller-container {
@@ -65,91 +56,11 @@ body.fixed-layout {
flex-shrink: 0;
}
.query-metadata__bottom {
margin: 0 10px;
}
.bottom-controller, .bottom-controller-container {
.query-metadata__property {
margin-right: 5px;
}
}
// Editor
edit-in-place p, span.editable {
display: inline-block;
}
edit-in-place p.editable:hover {
display: inline-block;
}
.editor__control {
margin-top: 10px;
.dropdown-toggle {
margin-right: 0;
}
}
.filter-container {
margin-bottom: 5px;
}
.ace_editor.ace_autocomplete .ace_completion-highlight {
text-shadow: none !important;
background: #ffff005e;
font-weight: 600;
}
.query-metadata {
background: #fff;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
table {
width: 100%;
white-space: nowrap;
text-overflow: ellipsis;
td {
padding: 3px 0;
vertical-align: top;
}
tr {
td:first-of-type {
padding-right: 5px;
}
}
}
p {
&:last-of-type {
margin-bottom: 0;
}
display: flex;
justify-content: space-between;
}
.query-metadata__property {
width: 60px;
display: inline-block;
}
._query-metadata__time {
}
}
.editor__control {
.form-control {
height: 30px;
}
}
.schema-container {
background: transparent;
flex-grow: 1;
@@ -159,7 +70,7 @@ edit-in-place p.editable:hover {
.editor__left {
height: 100% !important;
width: calc(~'25% - 10px');
width: calc(~"25% - 10px");
margin-right: 10px;
.form-control {
@@ -191,7 +102,6 @@ edit-in-place p.editable:hover {
}
.embed__vis {
}
.query__vis {
@@ -246,6 +156,17 @@ edit-in-place p.editable:hover {
margin-left: 15px;
margin-right: 15px;
}
.tags-control a {
opacity: 0;
transition: opacity 0.2s ease-in-out;
}
&:hover {
.tags-control a {
opacity: 1;
}
}
}
a.label-tag {
@@ -276,7 +197,22 @@ a.label-tag {
display: flex;
width: 100vw;
.tile, .tiled {
.resizable-component.react-resizable {
.react-resizable-handle-horizontal {
border-right: 1px solid #efefef;
}
.react-resizable-handle-vertical {
border-bottom: 1px solid #efefef;
}
}
.query-metadata.query-metadata-horizontal {
border-bottom: 1px solid #efefef;
}
.tile,
.tiled {
box-shadow: none;
padding: 15px 0 !important;
}
@@ -291,22 +227,26 @@ a.label-tag {
min-width: 10px;
overflow-x: hidden;
.schema-container {
}
.editor__left__data-source,
.schema-control,
.query-metadata--history,
.editor {
flex-shrink: 0;
}
.query-metadata {
border-top: 1px solid #efefef;
.editor__left__schema,
.editor__left__data-source {
padding: 15px;
}
.query-metadata, .editor__left__schema, .editor__left__data-source {
padding: 15px;
.editor__left__data-source {
.ant-select {
.ant-select-selection-selected-value {
img,
span {
vertical-align: middle;
}
}
}
}
.editor__left__schema {
@@ -317,7 +257,7 @@ a.label-tag {
padding-top: 0 !important;
position: relative;
schema-browser {
.schema-container {
position: absolute;
left: 15px;
top: 0;
@@ -326,10 +266,6 @@ a.label-tag {
}
}
}
main {
display: flex;
height: 100%;
}
.content {
background: #fff;
flex-grow: 1;
@@ -340,22 +276,13 @@ a.label-tag {
padding: 0;
overflow-x: hidden;
.editor {
border-bottom: 1px solid #efefef;
}
.pivot-table-visualization-container > table,
.visualization-renderer > .visualization-renderer-wrapper {
overflow: visible;
}
.tab-nav {
flex-shrink: 0;
}
}
.row {
background: #fff;
z-index: 9;
min-height: 50px;
&.resizable {
@@ -368,6 +295,10 @@ a.label-tag {
justify-content: space-around;
align-content: space-around;
overflow: hidden;
min-height: 10px;
max-height: 70vh;
flex: 0 0 300px;
}
.row {
@@ -394,7 +325,10 @@ a.label-tag {
transition: none !important;
}
}
.rg-right, .rg-left, .rg-top, .rg-bottom {
.rg-right,
.rg-left,
.rg-top,
.rg-bottom {
display: block;
width: 10px;
height: 10px;
@@ -409,32 +343,34 @@ a.label-tag {
border: 1px solid #ccc;
}
}
.rg-right, .rg-left {
.rg-right,
.rg-left {
span {
border-width: 0 1px;
top: 50%;
margin: -10px 0 0 @spacing/4;
margin: -10px 0 0 @spacing / 4;
height: 20px;
width: 3px;
}
}
.rg-top, .rg-bottom {
.rg-top,
.rg-bottom {
span {
border-width: 1px 0;
left: 50%;
margin: @spacing/4 0 0 -10px;
margin: @spacing / 4 0 0 -10px;
width: 20px;
height: 3px;
}
}
.rg-top {
.rg-top {
cursor: row-resize;
width: 100%;
top: 0;
left: 0;
margin-top: -@spacing/2;
margin-top: -@spacing / 2;
}
.rg-right {
.rg-right {
cursor: col-resize;
border-right: 1px solid #efefef;
height: 100%;
@@ -446,7 +382,7 @@ a.label-tag {
background: fade(@redash-gray, 6%);
}
}
.rg-bottom {
.rg-bottom {
cursor: row-resize;
background: #fff;
width: 100%;
@@ -458,7 +394,7 @@ a.label-tag {
background: fade(@redash-gray, 6%);
}
}
.rg-left {
.rg-left {
cursor: col-resize;
height: 100%;
left: 0;
@@ -471,11 +407,6 @@ a.label-tag {
visibility: hidden;
}
.query-fullscreen .query-metadata__mobile {
display: none;
}
// Visualization editor
.modal-xl .modal-content {
border: none;
@@ -512,32 +443,6 @@ nav .rg-bottom {
visibility: hidden;
}
.query-metadata--description {
max-height: 125px;
overflow-y: auto;
.edit-in-place.active {
width: 100% !important;
}
.edit-in-place .rd-form-control {
width: 100% !important;
}
}
.query-metadata--refresh {
height: 50px;
border: none !important;
.query-metadata__property {
width: auto;
}
p {
display: block;
}
}
.query-tags {
display: inline-block;
vertical-align: middle;
@@ -546,7 +451,6 @@ nav .rg-bottom {
.query-tags__mobile {
display: none;
margin: -5px 0 0 0;
padding: 0 0 0 23px;
}
@@ -561,7 +465,15 @@ nav .rg-bottom {
}
.edit-visualization {
margin-right: 5px;
margin-right: 5px;
}
@media (min-width: 880px) {
.query-fullscreen {
.query-metadata.query-metadata-horizontal {
display: none;
}
}
}
// Smaller screens
@@ -591,10 +503,6 @@ nav .rg-bottom {
display: none;
}
.tab-nav .tab-new-vis {
display: none;
}
.query-fullscreen {
flex-direction: column;
overflow: hidden;
@@ -607,25 +515,6 @@ nav .rg-bottom {
display: none;
}
.query-metadata__mobile {
border-bottom: 1px solid #efefef;
min-height: 0 !important;
flex-shrink: 0;
padding: 10px 15px;
display: flex;
flex-wrap: nowrap;
justify-content: space-between;
align-items: center;
.profile__image_thumb {
margin: 0 5px 0 0;
}
.query-metadata__property {
white-space: nowrap;
}
}
main {
flex-direction: column-reverse;
@@ -671,7 +560,8 @@ nav .rg-bottom {
}
@media (max-width: 768px) {
.editor__left__schema, .editor__left__data-source {
.editor__left__schema,
.editor__left__data-source {
display: none;
}
@@ -686,9 +576,5 @@ nav .rg-bottom {
h3 {
font-size: 18px;
}
favorites-control {
margin-top: -3px;
}
}
}

View File

@@ -14,9 +14,3 @@
opacity: 0.4;
}
}
// This is for using .inline-tags-control in Angular which renders
// a little differently than React (e.g. in Alert.html)
.inline-tags-control .tags-control {
display: inline-block;
}

View File

@@ -1,7 +1,7 @@
import React, { forwardRef } from 'react';
import AceEditor from 'react-ace';
import React, { forwardRef } from "react";
import AceEditor from "react-ace";
import './AceEditorInput.less';
import "./AceEditorInput.less";
function AceEditorInput(props, ref) {
return (

View File

@@ -1,12 +1,12 @@
import React, { useState, useMemo, useCallback, useEffect } from 'react';
import PropTypes from 'prop-types';
import { isEmpty, template } from 'lodash';
import React, { useState, useMemo, useCallback, useEffect } from "react";
import PropTypes from "prop-types";
import { isEmpty, template } from "lodash";
import Dropdown from 'antd/lib/dropdown';
import Icon from 'antd/lib/icon';
import Menu from 'antd/lib/menu';
import Dropdown from "antd/lib/dropdown";
import Icon from "antd/lib/icon";
import Menu from "antd/lib/menu";
import HelpTrigger from '@/components/HelpTrigger';
import HelpTrigger from "@/components/HelpTrigger";
export default function FavoritesDropdown({ fetch, urlTemplate }) {
const [items, setItems] = useState();
@@ -15,19 +15,24 @@ export default function FavoritesDropdown({ fetch, urlTemplate }) {
const noItems = isEmpty(items);
const urlCompiled = useMemo(() => template(urlTemplate), [urlTemplate]);
const fetchItems = useCallback(() => {
setLoading(true);
fetch().$promise
.then(({ results }) => {
setItems(results);
})
.finally(() => {
setLoading(false);
});
}, [fetch]);
const fetchItems = useCallback(
(showLoadingState = true) => {
setLoading(showLoadingState);
fetch()
.then(({ results }) => {
setItems(results);
})
.finally(() => {
setLoading(false);
});
},
[fetch]
);
// fetch items on init
useEffect(fetchItems, []);
useEffect(() => {
fetchItems(false);
}, [fetchItems]);
// fetch items on click
const onVisibleChange = visible => visible && fetchItems();
@@ -57,7 +62,12 @@ export default function FavoritesDropdown({ fetch, urlTemplate }) {
);
return (
<Dropdown disabled={loading} trigger={['click']} placement="bottomLeft" onVisibleChange={onVisibleChange} overlay={menu}>
<Dropdown
disabled={loading}
trigger={["click"]}
placement="bottomLeft"
onVisibleChange={onVisibleChange}
overlay={menu}>
{loading ? <Icon type="loading" spin /> : <Icon type="down" />}
</Dropdown>
);

View File

@@ -1,80 +1,85 @@
/* eslint-disable no-template-curly-in-string */
import React, { useRef } from 'react';
import { react2angular } from 'react2angular';
import React, { useCallback, useRef } from "react";
import Dropdown from 'antd/lib/dropdown';
import Button from 'antd/lib/button';
import Icon from 'antd/lib/icon';
import Menu from 'antd/lib/menu';
import Input from 'antd/lib/input';
import Tooltip from 'antd/lib/tooltip';
import Dropdown from "antd/lib/dropdown";
import Button from "antd/lib/button";
import Icon from "antd/lib/icon";
import Menu from "antd/lib/menu";
import Input from "antd/lib/input";
import Tooltip from "antd/lib/tooltip";
import FavoritesDropdown from './components/FavoritesDropdown';
import HelpTrigger from '@/components/HelpTrigger';
import CreateDashboardDialog from '@/components/dashboards/CreateDashboardDialog';
import HelpTrigger from "@/components/HelpTrigger";
import CreateDashboardDialog from "@/components/dashboards/CreateDashboardDialog";
import navigateTo from "@/components/ApplicationArea/navigateTo";
import { currentUser, Auth, clientConfig } from '@/services/auth';
import { $location, $route } from '@/services/ng';
import { Dashboard } from '@/services/dashboard';
import { Query } from '@/services/query';
import frontendVersion from '@/version.json';
import logoUrl from '@/assets/images/redash_icon_small.png';
import { currentUser, Auth, clientConfig } from "@/services/auth";
import { Dashboard } from "@/services/dashboard";
import { Query } from "@/services/query";
import frontendVersion from "@/version.json";
import logoUrl from "@/assets/images/redash_icon_small.png";
import './AppHeader.less';
import FavoritesDropdown from "./FavoritesDropdown";
import "./index.less";
function onSearch(q) {
$location.path('/queries').search({ q });
$route.reload();
navigateTo(`queries?q=${encodeURIComponent(q)}`);
}
function DesktopNavbar() {
const showCreateDashboardDialog = useCallback(() => {
CreateDashboardDialog.showModal().result.catch(() => {}); // ignore dismiss
}, []);
return (
<div className="app-header" data-platform="desktop">
<div>
<Menu mode="horizontal" selectable={false}>
{currentUser.hasPermission('list_dashboards') && (
{currentUser.hasPermission("list_dashboards") && (
<Menu.Item key="dashboards" className="dropdown-menu-item">
<Button href="dashboards">Dashboards</Button>
<FavoritesDropdown fetch={Dashboard.favorites} urlTemplate="dashboard/${slug}" />
</Menu.Item>
)}
{currentUser.hasPermission('view_query') && (
{currentUser.hasPermission("view_query") && (
<Menu.Item key="queries" className="dropdown-menu-item">
<Button href="queries">Queries</Button>
<FavoritesDropdown fetch={Query.favorites} urlTemplate="queries/${id}" />
</Menu.Item>
)}
{currentUser.hasPermission('list_alerts') && (
{currentUser.hasPermission("list_alerts") && (
<Menu.Item key="alerts">
<Button href="alerts">Alerts</Button>
</Menu.Item>
)}
</Menu>
<Dropdown
trigger={['click']}
overlay={(
<Menu>
{currentUser.hasPermission('create_query') && (
<Menu.Item key="new-query">
<a href="queries/new">New Query</a>
</Menu.Item>
)}
{currentUser.hasPermission('create_dashboard') && (
<Menu.Item key="new-dashboard">
<a onMouseUp={CreateDashboardDialog.showModal}>New Dashboard</a>
</Menu.Item>
)}
<Menu.Item key="new-alert">
<a href="alerts/new">New Alert</a>
</Menu.Item>
</Menu>
)}
>
<Button type="primary" data-test="CreateButton">
Create <Icon type="down" />
</Button>
</Dropdown>
{currentUser.canCreate() && (
<Dropdown
trigger={["click"]}
overlay={
<Menu>
{currentUser.hasPermission("create_query") && (
<Menu.Item key="new-query">
<a href="queries/new">New Query</a>
</Menu.Item>
)}
{currentUser.hasPermission("create_dashboard") && (
<Menu.Item key="new-dashboard">
<a onMouseUp={showCreateDashboardDialog}>New Dashboard</a>
</Menu.Item>
)}
{currentUser.hasPermission("list_alerts") && (
<Menu.Item key="new-alert">
<a href="alerts/new">New Alert</a>
</Menu.Item>
)}
</Menu>
}>
<Button type="primary" data-test="CreateButton">
Create <Icon type="down" />
</Button>
</Dropdown>
)}
</div>
<div className="header-logo">
<a href="./">
@@ -105,65 +110,68 @@ function DesktopNavbar() {
<Dropdown
overlayStyle={{ minWidth: 200 }}
placement="bottomRight"
trigger={['click']}
overlay={(
trigger={["click"]}
overlay={
<Menu>
<Menu.Item key="profile">
<a href="users/me">Edit Profile</a>
</Menu.Item>
{currentUser.hasPermission('super_admin') && (
<Menu.Divider />
)}
{currentUser.hasPermission("super_admin") && <Menu.Divider />}
{currentUser.isAdmin && (
<Menu.Item key="datasources">
<a href="data_sources">Data Sources</a>
</Menu.Item>
)}
{currentUser.hasPermission('list_users') && (
{currentUser.hasPermission("list_users") && (
<Menu.Item key="groups">
<a href="groups">Groups</a>
</Menu.Item>
)}
{currentUser.hasPermission('list_users') && (
{currentUser.hasPermission("list_users") && (
<Menu.Item key="users">
<a href="users">Users</a>
</Menu.Item>
)}
<Menu.Item key="snippets">
<a href="query_snippets">Query Snippets</a>
</Menu.Item>
{currentUser.hasPermission('list_users') && (
{currentUser.hasPermission("create_query") && (
<Menu.Item key="snippets">
<a href="query_snippets">Query Snippets</a>
</Menu.Item>
)}
{currentUser.isAdmin && (
<Menu.Item key="destinations">
<a href="destinations">Alert Destinations</a>
</Menu.Item>
)}
{currentUser.hasPermission('super_admin') && (
<Menu.Divider />
)}
{currentUser.hasPermission('super_admin') && (
{currentUser.hasPermission("super_admin") && <Menu.Divider />}
{currentUser.hasPermission("super_admin") && (
<Menu.Item key="status">
<a href="admin/status">System Status</a>
</Menu.Item>
)}
<Menu.Divider />
<Menu.Item key="logout" onClick={() => Auth.logout()}>Log out</Menu.Item>
<Menu.Item key="logout" onClick={() => Auth.logout()}>
Log out
</Menu.Item>
<Menu.Divider />
<Menu.Item key="version" disabled>
Version: {clientConfig.version}
{frontendVersion !== clientConfig.version && ` (${frontendVersion.substring(0, 8)})`}
{clientConfig.newVersionAvailable && currentUser.hasPermission('super_admin') && (
{clientConfig.newVersionAvailable && currentUser.hasPermission("super_admin") && (
<Tooltip title="Update Available" placement="rightTop">
{' '}
{/* eslint-disable-next-line react/jsx-no-target-blank */}
<a href="https://version.redash.io/" className="update-available" target="_blank" rel="noopener">
{" "}
{/* eslint-disable react/jsx-no-target-blank */}
<a
href="https://version.redash.io/"
className="update-available"
target="_blank"
rel="noopener">
<i className="fa fa-arrow-circle-down" />
</a>
</Tooltip>
)}
</Menu.Item>
</Menu>
)}
>
}>
<Button data-test="ProfileDropdown" className="profile-dropdown">
<img src={currentUser.profile_image_url} alt={currentUser.name} />
<span>{currentUser.name}</span>
@@ -190,21 +198,21 @@ function MobileNavbar() {
<div>
<Dropdown
overlayStyle={{ minWidth: 200 }}
trigger={['click']}
trigger={["click"]}
getPopupContainer={() => ref.current} // so the overlay menu stays with the fixed header when page scrolls
overlay={(
overlay={
<Menu mode="vertical" selectable={false}>
{currentUser.hasPermission('list_dashboards') && (
{currentUser.hasPermission("list_dashboards") && (
<Menu.Item key="dashboards">
<a href="dashboards">Dashboards</a>
</Menu.Item>
)}
{currentUser.hasPermission('view_query') && (
{currentUser.hasPermission("view_query") && (
<Menu.Item key="queries">
<a href="queries">Queries</a>
</Menu.Item>
)}
{currentUser.hasPermission('list_alerts') && (
{currentUser.hasPermission("list_alerts") && (
<Menu.Item key="alerts">
<a href="alerts">Alerts</a>
</Menu.Item>
@@ -218,30 +226,33 @@ function MobileNavbar() {
<a href="data_sources">Settings</a>
</Menu.Item>
)}
{currentUser.hasPermission('super_admin') && (
{currentUser.hasPermission("super_admin") && (
<Menu.Item key="status">
<a href="admin/status">System Status</a>
</Menu.Item>
)}
{currentUser.hasPermission('super_admin') && (
<Menu.Divider />
)}
{currentUser.hasPermission("super_admin") && <Menu.Divider />}
<Menu.Item key="help">
{/* eslint-disable-next-line react/jsx-no-target-blank */}
<a href="https://redash.io/help" target="_blank" rel="noopener">Help</a>
<a href="https://redash.io/help" target="_blank" rel="noopener">
Help
</a>
</Menu.Item>
<Menu.Item key="logout" onClick={() => Auth.logout()}>
Log out
</Menu.Item>
<Menu.Item key="logout" onClick={() => Auth.logout()}>Log out</Menu.Item>
</Menu>
)}
>
<Button><Icon type="menu" /></Button>
}>
<Button>
<Icon type="menu" />
</Button>
</Dropdown>
</div>
</div>
);
}
export function AppHeader() {
export default function ApplicationHeader() {
return (
<nav className="app-header-wrapper">
<DesktopNavbar />
@@ -249,9 +260,3 @@ export function AppHeader() {
</nav>
);
}
export default function init(ngModule) {
ngModule.component('appHeader', react2angular(AppHeader));
}
init.init = true;

View File

@@ -8,13 +8,13 @@ nav .app-header {
justify-content: space-between;
margin-bottom: 10px;
background: white;
box-shadow: 0 4px 9px -3px rgba(102, 136, 153, .15);
box-shadow: 0 4px 9px -3px rgba(102, 136, 153, 0.15);
.darker {
color: #333 !important;
&:hover {
color: #2196F3 !important;
color: #2196f3 !important;
}
}
@@ -39,7 +39,7 @@ nav .app-header {
height: 50px;
border-bottom: 0;
}
.ant-btn {
font-weight: 500;
@@ -70,7 +70,7 @@ nav .app-header {
top: 2px;
svg {
transition: transform .2s cubic-bezier(.75,0,.25,1);
transition: transform 0.2s cubic-bezier(0.75, 0, 0.25, 1);
}
}
@@ -140,7 +140,7 @@ nav .app-header {
.menu-item-button {
padding: 0 10px;
}
.ant-menu-root {
margin: 0 5px;
}
@@ -198,10 +198,10 @@ nav .app-header {
.ant-dropdown-menu-item .help-trigger {
display: inline;
color: #2196F3;
color: #2196f3;
vertical-align: bottom;
}
.ant-dropdown-menu.favorites-dropdown {
margin-left: -10px;
}
}

View File

@@ -0,0 +1,59 @@
import { isObject, get } from "lodash";
import React from "react";
import PropTypes from "prop-types";
function getErrorMessageByStatus(status, defaultMessage) {
switch (status) {
case 404:
return "It seems like the page you're looking for cannot be found.";
case 401:
case 403:
return "It seems like you dont have permission to see this page.";
default:
return defaultMessage;
}
}
export function getErrorMessage(error) {
const message = "It seems like we encountered an error. Try refreshing this page or contact your administrator.";
if (isObject(error)) {
// HTTP errors
if (error.isAxiosError && isObject(error.response)) {
return getErrorMessageByStatus(error.response.status, get(error, "response.data.message", message));
}
// Router errors
if (error.status) {
return getErrorMessageByStatus(error.status, message);
}
}
return message;
}
export default function ErrorMessage({ error }) {
if (!error) {
return null;
}
console.error(error);
return (
<div className="fixed-container" data-test="ErrorMessage">
<div className="container">
<div className="col-md-8 col-md-push-2">
<div className="error-state bg-white tiled">
<div className="error-state__icon">
<i className="zmdi zmdi-alert-circle-o" />
</div>
<div className="error-state__details">
<h4>{getErrorMessage(error)}</h4>
</div>
</div>
</div>
</div>
</div>
);
}
ErrorMessage.propTypes = {
error: PropTypes.object.isRequired,
};

View File

@@ -0,0 +1,151 @@
import { isFunction, map, fromPairs, extend, startsWith, trimStart, trimEnd } from "lodash";
import React, { useState, useEffect, useRef } from "react";
import PropTypes from "prop-types";
import UniversalRouter from "universal-router";
import ErrorBoundary from "@/components/ErrorBoundary";
import location from "@/services/location";
import url from "@/services/url";
import ErrorMessage from "./ErrorMessage";
function generateRouteKey() {
return Math.random()
.toString(32)
.substr(2);
}
export function stripBase(href) {
// Resolve provided link and '' (root) relative to document's base.
// If provided href is not related to current document (does not
// start with resolved root) - return false. Otherwise
// strip root and return relative url.
const baseHref = trimEnd(url.normalize(""), "/") + "/";
href = url.normalize(href);
if (startsWith(href, baseHref)) {
return "/" + trimStart(href.substr(baseHref.length), "/");
}
return false;
}
function resolveRouteDependencies(route) {
return Promise.all(
map(route.resolve, (value, key) => {
value = isFunction(value) ? value(route.routeParams, route, location) : value;
return Promise.resolve(value).then(result => [key, result]);
})
).then(results => {
route.routeParams = extend(route.routeParams, fromPairs(results));
return route;
});
}
export default function Router({ routes, onRouteChange }) {
const [currentRoute, setCurrentRoute] = useState(null);
const currentPathRef = useRef(null);
const errorHandlerRef = useRef();
useEffect(() => {
let isAbandoned = false;
const router = new UniversalRouter(routes, {
resolveRoute({ route }, routeParams) {
if (isFunction(route.render)) {
return { ...route, routeParams };
}
},
});
function resolve(action) {
if (!isAbandoned) {
if (errorHandlerRef.current) {
errorHandlerRef.current.reset();
}
const pathname = stripBase(location.path);
// This is a optimization for route resolver: if current route was already resolved
// from this path - do nothing. It also prevents router from using outdated route in a case
// when user navigated to another path while current one was still resolving.
// Note: this lock uses only `path` fragment of URL to distinguish routes because currently
// all pages depend only on this fragment and handle search/hash on their own. If router
// should reload page on search/hash change - this fragment (and few checks below) should be updated
if (pathname === currentPathRef.current) {
return;
}
currentPathRef.current = pathname;
// Don't reload controller if URL was replaced
if (action === "REPLACE") {
return;
}
router
.resolve({ pathname })
.then(route => {
return isAbandoned || currentPathRef.current !== pathname ? null : resolveRouteDependencies(route);
})
.then(route => {
if (route) {
setCurrentRoute({ ...route, key: generateRouteKey() });
}
})
.catch(error => {
if (!isAbandoned && currentPathRef.current === pathname) {
setCurrentRoute({
render: currentRoute => <ErrorMessage {...currentRoute.routeParams} />,
routeParams: { error },
});
}
});
}
}
resolve("PUSH");
const unlisten = location.listen((unused, action) => resolve(action));
return () => {
isAbandoned = true;
unlisten();
};
}, [routes]);
useEffect(() => {
onRouteChange(currentRoute);
}, [currentRoute, onRouteChange]);
if (!currentRoute) {
return null;
}
return (
<ErrorBoundary ref={errorHandlerRef} renderError={error => <ErrorMessage error={error} />}>
{currentRoute.render(currentRoute)}
</ErrorBoundary>
);
}
Router.propTypes = {
routes: PropTypes.arrayOf(
PropTypes.shape({
path: PropTypes.string.isRequired,
render: PropTypes.func, // (routeParams: PropTypes.object; currentRoute; location) => PropTypes.node
// Additional props to be injected into route component.
// Object keys are props names. Object values will become prop values:
// - if value is a function - it will be called without arguments, and result will be used; otherwise value will be used;
// - after previous step, if value is a promise - router will wait for it to resolve; resolved value then will be used;
// otherwise value will be used directly.
resolve: PropTypes.objectOf(PropTypes.any),
})
),
onRouteChange: PropTypes.func,
};
Router.defaultProps = {
routes: [],
onRouteChange: () => {},
};

View File

@@ -0,0 +1,29 @@
import { isString } from "lodash";
import navigateTo from "./navigateTo";
export default function handleNavigationIntent(event) {
let element = event.target;
while (element) {
if (element.tagName === "A") {
break;
}
element = element.parentNode;
}
if (!element || !element.hasAttribute("href")) {
return;
}
// Keep some default behaviour
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
return;
}
const target = element.getAttribute("target");
if (isString(target) && target.toLowerCase() === "_blank") {
return;
}
event.preventDefault();
navigateTo(element.href);
}

View File

@@ -0,0 +1,37 @@
import React, { useState, useEffect } from "react";
import routes from "@/pages";
import Router from "./Router";
import handleNavigationIntent from "./handleNavigationIntent";
import ErrorMessage from "./ErrorMessage";
export default function ApplicationArea() {
const [currentRoute, setCurrentRoute] = useState(null);
const [unhandledError, setUnhandledError] = useState(null);
useEffect(() => {
if (currentRoute && currentRoute.title) {
document.title = currentRoute.title;
}
}, [currentRoute]);
useEffect(() => {
function globalErrorHandler(event) {
event.preventDefault();
setUnhandledError(event.error);
}
document.body.addEventListener("click", handleNavigationIntent, false);
window.addEventListener("error", globalErrorHandler, false);
return () => {
document.body.removeEventListener("click", handleNavigationIntent, false);
window.removeEventListener("error", globalErrorHandler, false);
};
}, []);
if (unhandledError) {
return <ErrorMessage error={unhandledError} />;
}
return <Router routes={routes} onRouteChange={setCurrentRoute} />;
}

View File

@@ -0,0 +1,25 @@
import location from "@/services/location";
import url from "@/services/url";
import { stripBase } from "./Router";
// When `replace` is set to `true` - it will just replace current URL
// without reloading current page (router will skip this location change)
export default function navigateTo(href, replace = false) {
// Allow calling chain to roll up, and then navigate
setTimeout(() => {
const isExternal = stripBase(href) === false;
if (isExternal) {
window.location = href;
return;
}
href = url.parse(href);
location.update(
{
path: href.pathname,
search: href.search,
hash: href.hash,
},
replace
);
}, 10);
}

View File

@@ -0,0 +1,63 @@
import React, { useEffect, useState, useContext } from "react";
import PropTypes from "prop-types";
import { ErrorBoundaryContext } from "@/components/ErrorBoundary";
import { Auth } from "@/services/auth";
// This wrapper modifies `route.render` function and instead of passing `currentRoute` passes an object
// that contains:
// - `currentRoute.routeParams`
// - `pageTitle` field which is equal to `currentRoute.title`
// - `onError` field which is a `handleError` method of nearest error boundary
// - `apiKey` field
function ApiKeySessionWrapper({ apiKey, currentRoute, renderChildren }) {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const { handleError } = useContext(ErrorBoundaryContext);
useEffect(() => {
let isCancelled = false;
Auth.setApiKey(apiKey);
Auth.loadConfig()
.then(() => {
if (!isCancelled) {
setIsAuthenticated(true);
}
})
.catch(() => {
if (!isCancelled) {
setIsAuthenticated(false);
}
});
return () => {
isCancelled = true;
};
}, [apiKey]);
if (!isAuthenticated) {
return null;
}
return (
<React.Fragment key={currentRoute.key}>
{renderChildren({ ...currentRoute.routeParams, pageTitle: currentRoute.title, onError: handleError, apiKey })}
</React.Fragment>
);
}
ApiKeySessionWrapper.propTypes = {
apiKey: PropTypes.string.isRequired,
renderChildren: PropTypes.func,
};
ApiKeySessionWrapper.defaultProps = {
renderChildren: () => null,
};
export default function routeWithApiKeySession({ render, getApiKey, ...rest }) {
return {
...rest,
render: currentRoute => (
<ApiKeySessionWrapper apiKey={getApiKey(currentRoute)} currentRoute={currentRoute} renderChildren={render} />
),
};
}

View File

@@ -0,0 +1,82 @@
import React, { useEffect, useState } from "react";
import PropTypes from "prop-types";
import ErrorBoundary, { ErrorBoundaryContext } from "@/components/ErrorBoundary";
import { Auth } from "@/services/auth";
import organizationStatus from "@/services/organizationStatus";
import ApplicationHeader from "./ApplicationHeader";
import ErrorMessage from "./ErrorMessage";
// This wrapper modifies `route.render` function and instead of passing `currentRoute` passes an object
// that contains:
// - `currentRoute.routeParams`
// - `pageTitle` field which is equal to `currentRoute.title`
// - `onError` field which is a `handleError` method of nearest error boundary
function UserSessionWrapper({ bodyClass, currentRoute, renderChildren }) {
const [isAuthenticated, setIsAuthenticated] = useState(!!Auth.isAuthenticated());
useEffect(() => {
let isCancelled = false;
Promise.all([Auth.requireSession(), organizationStatus.refresh()])
.then(() => {
if (!isCancelled) {
setIsAuthenticated(!!Auth.isAuthenticated());
}
})
.catch(() => {
if (!isCancelled) {
setIsAuthenticated(false);
}
});
return () => {
isCancelled = true;
};
}, []);
useEffect(() => {
if (bodyClass) {
document.body.classList.toggle(bodyClass, true);
return () => {
document.body.classList.toggle(bodyClass, false);
};
}
}, [bodyClass]);
if (!isAuthenticated) {
return null;
}
return (
<React.Fragment>
<ApplicationHeader />
<React.Fragment key={currentRoute.key}>
<ErrorBoundary renderError={error => <ErrorMessage error={error} />}>
<ErrorBoundaryContext.Consumer>
{({ handleError }) =>
renderChildren({ ...currentRoute.routeParams, pageTitle: currentRoute.title, onError: handleError })
}
</ErrorBoundaryContext.Consumer>
</ErrorBoundary>
</React.Fragment>
</React.Fragment>
);
}
UserSessionWrapper.propTypes = {
bodyClass: PropTypes.string,
renderChildren: PropTypes.func,
};
UserSessionWrapper.defaultProps = {
bodyClass: null,
renderChildren: () => null,
};
export default function routeWithUserSession({ render, bodyClass, ...rest }) {
return {
...rest,
render: currentRoute => (
<UserSessionWrapper bodyClass={bodyClass} currentRoute={currentRoute} renderChildren={render} />
),
};
}

View File

@@ -1,43 +0,0 @@
import React from 'react';
import Tooltip from 'antd/lib/tooltip';
import PropTypes from 'prop-types';
import '@/redash-font/style.less';
import recordEvent from '@/services/recordEvent';
export default function AutocompleteToggle({ state, disabled, onToggle }) {
let tooltipMessage = 'Live Autocomplete Enabled';
let icon = 'icon-flash';
if (!state) {
tooltipMessage = 'Live Autocomplete Disabled';
icon = 'icon-flash-off';
}
if (disabled) {
tooltipMessage = 'Live Autocomplete Not Available (Use Ctrl+Space to Trigger)';
icon = 'icon-flash-off';
}
const toggle = (newState) => {
recordEvent('toggle_autocomplete', 'screen', 'query_editor', { state: newState });
onToggle(newState);
};
return (
<Tooltip placement="top" title={tooltipMessage}>
<button
type="button"
className={'btn btn-default m-r-5' + (disabled ? ' disabled' : '')}
onClick={() => toggle(!state)}
disabled={disabled}
>
<i className={'icon ' + icon} />
</button>
</Tooltip>
);
}
AutocompleteToggle.propTypes = {
state: PropTypes.bool.isRequired,
disabled: PropTypes.bool.isRequired,
onToggle: PropTypes.func.isRequired,
};

View File

@@ -1,16 +1,15 @@
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';
import React, { useState } from "react";
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() {
function BeaconConsent() {
const [hide, setHide] = useState(false);
if (!clientConfig.showBeaconConsentMessage || hide) {
@@ -22,11 +21,11 @@ export function BeaconConsent() {
setHide(true);
};
const confirmConsent = (confirm) => {
let message = '🙏 Thank you.';
const confirmConsent = confirm => {
let message = "🙏 Thank you.";
if (!confirm) {
message = 'Settings Saved.';
message = "Settings Saved.";
}
OrgSettings.save({ beacon_consent: confirm }, message)
@@ -41,14 +40,13 @@ export function BeaconConsent() {
<DynamicComponent name="BeaconConsent">
<div className="m-t-10 tiled">
<Card
title={(
title={
<>
Would you be ok with sharing anonymous usage data with the Redash team?{' '}
Would you be ok with sharing anonymous usage data with the Redash team?{" "}
<HelpTrigger type="USAGE_DATA_SHARING" />
</>
)}
bordered={false}
>
}
bordered={false}>
<Text>Help Redash improve by automatically sending anonymous usage data:</Text>
<div className="m-t-5">
<ul>
@@ -67,7 +65,8 @@ export function BeaconConsent() {
</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.
You can change this setting anytime from the <a href="settings/organization">Organization Settings</a>{" "}
page.
</Text>
</div>
</Card>
@@ -76,8 +75,4 @@ export function BeaconConsent() {
);
}
export default function init(ngModule) {
ngModule.component('beaconConsent', react2angular(BeaconConsent));
}
init.init = true;
export default BeaconConsent;

View File

@@ -1,12 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import React from "react";
import PropTypes from "prop-types";
export function BigMessage({ message, icon, children, className }) {
function BigMessage({ message, icon, children, className }) {
return (
<div className={'p-15 text-center ' + className}>
<div className={"p-15 text-center " + className}>
<h3 className="m-t-0 m-b-0">
<i className={'fa ' + icon} />
<i className={"fa " + icon} />
</h3>
<br />
{message}
@@ -23,13 +22,9 @@ BigMessage.propTypes = {
};
BigMessage.defaultProps = {
message: '',
message: "",
children: null,
className: 'tiled bg-white',
className: "tiled bg-white",
};
export default function init(ngModule) {
ngModule.component('bigMessage', react2angular(BigMessage));
}
init.init = true;
export default BigMessage;

View File

@@ -1,8 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import Button from 'antd/lib/button';
import Tooltip from 'antd/lib/tooltip';
import './CodeBlock.less';
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 = {
@@ -20,7 +20,7 @@ export default class CodeBlock extends React.Component {
constructor(props) {
super(props);
this.ref = React.createRef();
this.copyFeatureEnabled = props.copyable && document.queryCommandSupported('copy');
this.copyFeatureEnabled = props.copyable && document.queryCommandSupported("copy");
this.resetCopyState = null;
}
@@ -36,14 +36,14 @@ export default class CodeBlock extends React.Component {
// copy
try {
const success = document.execCommand('copy');
const success = document.execCommand("copy");
if (!success) {
throw new Error();
}
this.setState({ copied: 'Copied!' });
this.setState({ copied: "Copied!" });
} catch (err) {
this.setState({
copied: 'Copy failed',
copied: "Copy failed",
});
}
@@ -58,13 +58,8 @@ export default class CodeBlock extends React.Component {
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 title={this.state.copied || "Copy"}>
<Button icon="copy" type="dashed" size="small" onClick={this.copy} />
</Tooltip>
);

View File

@@ -1,12 +1,17 @@
import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import AntCollapse from 'antd/lib/collapse';
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
{...props}
activeKey={collapsed ? null : "content"}
className={cx(className, "ant-collapse-headerless")}>
<AntCollapse.Panel key="content" header="">
{children}
</AntCollapse.Panel>
</AntCollapse>
);
}
@@ -20,5 +25,5 @@ Collapse.propTypes = {
Collapse.defaultProps = {
collapsed: true,
children: null,
className: '',
className: "",
};

View File

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

@@ -1,12 +1,12 @@
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 { 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';
import "./input.less";
function preparePresets(presetColors, presetColumns) {
presetColors = isArray(presetColors) ? map(presetColors, v => [null, v]) : toPairs(presetColors);
@@ -16,14 +16,14 @@ function preparePresets(presetColors, presetColumns) {
}
value = tinycolor(value);
if (value.isValid()) {
return [title, '#' + value.toHex().toUpperCase()];
return [title, "#" + value.toHex().toUpperCase()];
}
return null;
});
return chunk(filter(presetColors), presetColumns);
}
function validateColor(value, callback, prefix = '#') {
function validateColor(value, callback, prefix = "#") {
if (isNil(value)) {
callback(null);
}
@@ -34,7 +34,7 @@ function validateColor(value, callback, prefix = '#') {
}
export default function Input({ color, presetColors, presetColumns, onChange, onPressEnter }) {
const [inputValue, setInputValue] = useState('');
const [inputValue, setInputValue] = useState("");
const [isInputFocused, setIsInputFocused] = useState(false);
const presets = preparePresets(presetColors, presetColumns);
@@ -46,7 +46,7 @@ export default function Input({ color, presetColors, presetColumns, onChange, on
useEffect(() => {
if (!isInputFocused) {
validateColor(color, setInputValue, '');
validateColor(color, setInputValue, "");
}
}, [color, isInputFocused]);
@@ -61,6 +61,7 @@ export default function Input({ color, presetColors, presetColumns, onChange, on
))}
<div className="color-picker-input">
<TextInput
data-test="ColorPicker.CustomColor"
addonBefore={<Typography.Text type="secondary">#</Typography.Text>}
value={inputValue}
onChange={e => handleInputChange(e.target.value)}
@@ -85,7 +86,7 @@ Input.propTypes = {
};
Input.defaultProps = {
color: '#FFFFFF',
color: "#FFFFFF",
presetColors: null,
presetColumns: 8,
onChange: () => {},

View File

@@ -0,0 +1,31 @@
import React, { useMemo } from "react";
import PropTypes from "prop-types";
import cx from "classnames";
import { validateColor, getColorName } from "./utils";
import "./label.less";
export default function Label({ className, color, presetColors, ...props }) {
const name = useMemo(() => getColorName(validateColor(color), presetColors), [color, presetColors]);
return (
<span className={cx("color-label", className)} {...props}>
{name}
</span>
);
}
Label.propTypes = {
className: PropTypes.string,
color: PropTypes.string,
presetColors: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.string), // array of colors (no tooltips)
PropTypes.objectOf(PropTypes.string), // color name => color value
]),
};
Label.defaultProps = {
className: null,
color: "#FFFFFF",
presetColors: null,
};

View File

@@ -1,22 +1,21 @@
import { isString } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import Tooltip from 'antd/lib/tooltip';
import { isString } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import cx from "classnames";
import Tooltip from "antd/lib/tooltip";
import './swatch.less';
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}
/>
<span className={cx("color-swatch", className)} style={{ backgroundColor: color, width: size }} {...props} />
);
if (isString(title) && (title !== '')) {
if (isString(title) && title !== "") {
return (
<Tooltip title={title} mouseEnterDelay={0} mouseLeaveDelay={0}>{result}</Tooltip>
<Tooltip title={title} mouseEnterDelay={0} mouseLeaveDelay={0}>
{result}
</Tooltip>
);
}
return result;
@@ -30,8 +29,8 @@ Swatch.propTypes = {
};
Swatch.defaultProps = {
className: '',
className: null,
title: null,
color: 'transparent',
color: "transparent",
size: 12,
};

View File

@@ -1,27 +1,35 @@
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 { toString } from "lodash";
import React, { useState, useEffect, useMemo } from "react";
import PropTypes from "prop-types";
import cx from "classnames";
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 chooseTextColorForBackground from "@/lib/chooseTextColorForBackground";
import ColorInput from './Input';
import Swatch from './Swatch';
import ColorInput from "./Input";
import Swatch from "./Swatch";
import Label from "./Label";
import { validateColor } from "./utils";
import './index.less';
function validateColor(value, fallback = null) {
value = tinycolor(value);
return value.isValid() ? '#' + value.toHex().toUpperCase() : fallback;
}
import "./index.less";
export default function ColorPicker({
color, placement, presetColors, presetColumns, triggerSize, interactive, children, onChange,
color,
placement,
presetColors,
presetColumns,
interactive,
children,
onChange,
triggerProps,
addonBefore,
addonAfter,
}) {
const [visible, setVisible] = useState(false);
const [currentColor, setCurrentColor] = useState('');
const validatedColor = useMemo(() => validateColor(color), [color]);
const [currentColor, setCurrentColor] = useState("");
function handleApply() {
setVisible(false);
@@ -36,16 +44,16 @@ export default function ColorPicker({
const actions = [];
if (!interactive) {
actions.push((
actions.push(
<Tooltip key="cancel" title="Cancel">
<Icon type="close" onClick={handleCancel} />
</Tooltip>
));
actions.push((
);
actions.push(
<Tooltip key="apply" title="Apply">
<Icon type="check" onClick={handleApply} />
</Tooltip>
));
);
}
function handleInputChange(newColor) {
@@ -57,72 +65,97 @@ export default function ColorPicker({
useEffect(() => {
if (visible) {
setCurrentColor(validateColor(color));
setCurrentColor(validatedColor);
}
}, [color, visible]);
}, [validatedColor, 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}
<React.Fragment>
{addonBefore}
<Popover
arrowPointAtCenter
overlayClassName={`color-picker ${interactive ? "color-picker-interactive" : "color-picker-with-actions"}`}
overlayStyle={{ "--color-picker-selected-color": currentColor }}
content={
<Card
data-test="ColorPicker"
className="color-picker-panel"
bordered={false}
title={toString(currentColor).toUpperCase()}
headStyle={{
backgroundColor: currentColor,
color: chooseTextColorForBackground(currentColor),
}}
actions={actions}>
<ColorInput
color={currentColor}
presetColors={presetColors}
presetColumns={presetColumns}
onChange={handleInputChange}
onPressEnter={handleApply}
/>
</Card>
}
trigger="click"
placement={placement}
visible={visible}
onVisibleChange={setVisible}>
{children || (
<Swatch
color={validatedColor}
size={30}
{...triggerProps}
className={cx("color-picker-trigger", triggerProps.className)}
/>
</Card>
)}
trigger="click"
placement={placement}
visible={visible}
onVisibleChange={setVisible}
>
{children || (<Swatch className="color-picker-trigger" color={validateColor(color)} size={triggerSize} />)}
</Popover>
)}
</Popover>
{addonAfter}
</React.Fragment>
);
}
ColorPicker.propTypes = {
color: PropTypes.string,
placement: PropTypes.oneOf([
'top', 'left', 'right', 'bottom',
'topLeft', 'topRight', 'bottomLeft', 'bottomRight',
'leftTop', 'leftBottom', 'rightTop', 'rightBottom',
"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,
triggerProps: PropTypes.object, // eslint-disable-line react/forbid-prop-types
children: PropTypes.node,
addonBefore: PropTypes.node,
addonAfter: PropTypes.node,
onChange: PropTypes.func,
};
ColorPicker.defaultProps = {
color: '#FFFFFF',
placement: 'top',
color: "#FFFFFF",
placement: "top",
presetColors: null,
presetColumns: 8,
triggerSize: 30,
interactive: false,
triggerProps: {},
children: null,
addonBefore: null,
addonAfter: null,
onChange: () => {},
};
ColorPicker.Input = ColorInput;
ColorPicker.Swatch = Swatch;
ColorPicker.Label = Label;

View File

@@ -0,0 +1,7 @@
.color-label {
vertical-align: middle;
.color-swatch + & {
margin-left: 7px;
}
}

View File

@@ -0,0 +1,14 @@
import { isArray, findKey } from "lodash";
import tinycolor from "tinycolor2";
export function validateColor(value, fallback = null) {
value = tinycolor(value);
return value.isValid() ? "#" + value.toHex().toUpperCase() : fallback;
}
export function getColorName(color, presetColors) {
if (isArray(presetColors)) {
return color;
}
return findKey(presetColors, v => validateColor(v) === color) || color;
}

View File

@@ -1,17 +1,18 @@
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';
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 { getErrorMessage } from "@/components/ApplicationArea/ErrorMessage";
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;
@@ -38,19 +39,19 @@ class CreateSourceDialog extends React.Component {
};
state = {
searchText: '',
searchText: "",
selectedType: null,
savingSource: false,
currentStep: StepEnum.SELECT_TYPE,
};
selectType = (selectedType) => {
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 });
this.setState({ searchText: "", selectedType: null, currentStep: StepEnum.SELECT_TYPE });
}
};
@@ -58,21 +59,25 @@ class CreateSourceDialog extends React.Component {
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);
});
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(getErrorMessage(error.message));
});
}
};
renderTypeSelector() {
const { types } = this.props;
const { searchText } = this.state;
const filteredTypes = types.filter(type => isEmpty(searchText) ||
includes(type.name.toLowerCase(), searchText.toLowerCase()));
const filteredTypes = types.filter(
type => isEmpty(searchText) || includes(type.name.toLowerCase(), searchText.toLowerCase())
);
return (
<div className="m-t-10">
<Search
@@ -81,13 +86,11 @@ class CreateSourceDialog extends React.Component {
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 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>
@@ -102,12 +105,7 @@ class CreateSourceDialog extends React.Component {
return (
<div>
<div className="d-flex justify-content-center align-items-center">
<img
className="p-5"
src={`${imageFolder}/${selectedType.type}.png`}
alt={selectedType.name}
width="48"
/>
<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">
@@ -117,13 +115,7 @@ class CreateSourceDialog extends React.Component {
</HelpTrigger>
)}
</div>
<DynamicForm
id="sourceForm"
fields={fields}
onSubmit={this.createSource}
feedbackIcons
hideSubmitButton
/>
<DynamicForm id="sourceForm" fields={fields} onSubmit={this.createSource} feedbackIcons hideSubmitButton />
</div>
);
}
@@ -131,10 +123,7 @@ class CreateSourceDialog extends React.Component {
renderItem(item) {
const { imageFolder } = this.props;
return (
<List.Item
className="p-l-10 p-r-10 clickable"
onClick={() => this.selectType(item)}
>
<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>
@@ -149,34 +138,38 @@ class CreateSourceDialog extends React.Component {
<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>
),
]}
>
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={<a>Type Selection</a>} className="clickable" onClick={this.resetType} />
) : (
<Step title="Type Selection" />
)}
<Step title="Configuration" />
<Step title="Done" />
</Steps>

View File

@@ -1,17 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import DatePicker from 'antd/lib/date-picker';
import { clientConfig } from '@/services/auth';
import { Moment } from '@/components/proptypes';
import React from "react";
import PropTypes from "prop-types";
import DatePicker from "antd/lib/date-picker";
import { clientConfig } from "@/services/auth";
import { Moment } from "@/components/proptypes";
const DateInput = React.forwardRef(({
defaultValue,
value,
onSelect,
className,
...props
}, ref) => {
const format = clientConfig.dateFormat || 'YYYY-MM-DD';
const DateInput = React.forwardRef(({ defaultValue, value, onSelect, className, ...props }, ref) => {
const format = clientConfig.dateFormat || "YYYY-MM-DD";
const additionalAttributes = {};
if (defaultValue && defaultValue.isValid()) {
additionalAttributes.defaultValue = defaultValue;
@@ -43,7 +37,7 @@ DateInput.defaultProps = {
defaultValue: null,
value: undefined,
onSelect: () => {},
className: '',
className: "",
};
export default DateInput;

View File

@@ -1,20 +1,14 @@
import { isArray } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import DatePicker from 'antd/lib/date-picker';
import { clientConfig } from '@/services/auth';
import { Moment } from '@/components/proptypes';
import { isArray } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import DatePicker from "antd/lib/date-picker";
import { clientConfig } from "@/services/auth";
import { Moment } from "@/components/proptypes";
const { RangePicker } = DatePicker;
const DateRangeInput = React.forwardRef(({
defaultValue,
value,
onSelect,
className,
...props
}, ref) => {
const format = clientConfig.dateFormat || 'YYYY-MM-DD';
const DateRangeInput = React.forwardRef(({ defaultValue, value, onSelect, className, ...props }, ref) => {
const format = clientConfig.dateFormat || "YYYY-MM-DD";
const additionalAttributes = {};
if (isArray(defaultValue) && defaultValue[0].isValid() && defaultValue[1].isValid()) {
additionalAttributes.defaultValue = defaultValue;
@@ -45,7 +39,7 @@ DateRangeInput.defaultProps = {
defaultValue: null,
value: undefined,
onSelect: () => {},
className: '',
className: "",
};
export default DateRangeInput;

View File

@@ -1,19 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import DatePicker from 'antd/lib/date-picker';
import { clientConfig } from '@/services/auth';
import { Moment } from '@/components/proptypes';
import React from "react";
import PropTypes from "prop-types";
import DatePicker from "antd/lib/date-picker";
import { clientConfig } from "@/services/auth";
import { Moment } from "@/components/proptypes";
const DateTimeInput = React.forwardRef(({
defaultValue,
value,
withSeconds,
onSelect,
className,
...props
}, ref) => {
const format = (clientConfig.dateFormat || 'YYYY-MM-DD') +
(withSeconds ? ' HH:mm:ss' : ' HH:mm');
const DateTimeInput = React.forwardRef(({ defaultValue, value, withSeconds, onSelect, className, ...props }, ref) => {
const format = (clientConfig.dateFormat || "YYYY-MM-DD") + (withSeconds ? " HH:mm:ss" : " HH:mm");
const additionalAttributes = {};
if (defaultValue && defaultValue.isValid()) {
additionalAttributes.defaultValue = defaultValue;
@@ -48,7 +40,7 @@ DateTimeInput.defaultProps = {
value: undefined,
withSeconds: false,
onSelect: () => {},
className: '',
className: "",
};
export default DateTimeInput;

View File

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

View File

@@ -1,7 +1,7 @@
import { isFunction } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import { isFunction } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import ReactDOM from "react-dom";
/**
Wrapper for dialogs based on Ant's <Modal> component.
@@ -140,7 +140,7 @@ function openDialog(DialogComponent, props) {
reject: () => {},
};
const container = document.createElement('div');
const container = document.createElement("div");
document.body.appendChild(container);
function render() {
@@ -176,7 +176,7 @@ function openDialog(DialogComponent, props) {
const result = {
close: closeDialog,
dismiss: dismissDialog,
update: (newProps) => {
update: newProps => {
props = { ...props, ...newProps };
render();
},

View File

@@ -1,15 +1,15 @@
import { isFunction, isString } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import { isFunction, isString } from "lodash";
import React from "react";
import PropTypes from "prop-types";
const componentsRegistry = new Map();
const activeInstances = new Set();
export function registerComponent(name, component) {
if (isString(name) && name !== '') {
if (isString(name) && name !== "") {
componentsRegistry.set(name, isFunction(component) ? component : null);
// Refresh active DynamicComponent instances which use this component
activeInstances.forEach((dynamicComponent) => {
activeInstances.forEach(dynamicComponent => {
if (dynamicComponent.props.name === name) {
dynamicComponent.forceUpdate();
}

View File

@@ -1,23 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import { trim } from 'lodash';
import { trim } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import cx from "classnames";
import Input from "antd/lib/input";
export class EditInPlace extends React.Component {
export default class EditInPlace extends React.Component {
static propTypes = {
ignoreBlanks: PropTypes.bool,
isEditable: PropTypes.bool,
editor: PropTypes.string.isRequired,
placeholder: PropTypes.string,
value: PropTypes.string,
onDone: PropTypes.func.isRequired,
multiline: PropTypes.bool,
editorProps: PropTypes.object,
};
static defaultProps = {
ignoreBlanks: false,
isEditable: true,
placeholder: '',
value: '',
placeholder: "",
value: "",
multiline: false,
editorProps: {},
};
constructor(props) {
@@ -26,12 +30,12 @@ export class EditInPlace extends React.Component {
editing: false,
};
this.inputRef = React.createRef();
const self = this;
this.componentDidUpdate = (prevProps, prevState) => {
if (self.state.editing && !prevState.editing) {
self.inputRef.current.focus();
}
};
}
componentDidUpdate(_, prevState) {
if (this.state.editing && !prevState.editing) {
this.inputRef.current.focus();
}
}
startEditing = () => {
@@ -40,19 +44,19 @@ export class EditInPlace extends React.Component {
}
};
stopEditing = () => {
const newValue = trim(this.inputRef.current.value);
const ignorableBlank = this.props.ignoreBlanks && newValue === '';
stopEditing = currentValue => {
const newValue = trim(currentValue);
const ignorableBlank = this.props.ignoreBlanks && newValue === "";
if (!ignorableBlank && newValue !== this.props.value) {
this.props.onDone(newValue);
}
this.setState({ editing: false });
};
keyDown = (event) => {
handleKeyDown = event => {
if (event.keyCode === 13 && !event.shiftKey) {
event.preventDefault();
this.stopEditing();
this.stopEditing(event.target.value);
} else if (event.keyCode === 27) {
this.setState({ editing: false });
}
@@ -63,31 +67,30 @@ export class EditInPlace extends React.Component {
role="presentation"
onFocus={this.startEditing}
onClick={this.startEditing}
className={this.props.isEditable ? 'editable' : ''}
>
className={this.props.isEditable ? "editable" : ""}>
{this.props.value || this.props.placeholder}
</span>
);
renderEdit = () => React.createElement(this.props.editor, {
ref: this.inputRef,
className: 'rd-form-control',
defaultValue: this.props.value,
onBlur: this.stopEditing,
onKeyDown: this.keyDown,
});
renderEdit = () => {
const { multiline, value, editorProps } = this.props;
const InputComponent = multiline ? Input.TextArea : Input;
return (
<InputComponent
ref={this.inputRef}
defaultValue={value}
onBlur={e => this.stopEditing(e.target.value)}
onKeyDown={this.handleKeyDown}
{...editorProps}
/>
);
};
render() {
return (
<span className={'edit-in-place' + (this.state.editing ? ' active' : '')}>
<span className={cx("edit-in-place", { active: this.state.editing }, this.props.className)}>
{this.state.editing ? this.renderEdit() : this.renderNormal()}
</span>
);
}
}
export default function init(ngModule) {
ngModule.component('editInPlace', react2angular(EditInPlace));
}
init.init = true;

View File

@@ -1,23 +1,22 @@
import { includes, words, capitalize, clone, isNull } from 'lodash';
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import Checkbox from 'antd/lib/checkbox';
import Modal from 'antd/lib/modal';
import Form from 'antd/lib/form';
import Button from 'antd/lib/button';
import Select from 'antd/lib/select';
import Input from 'antd/lib/input';
import Divider from 'antd/lib/divider';
import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper';
import { QuerySelector } from '@/components/QuerySelector';
import { Query } from '@/services/query';
import { includes, words, capitalize, clone, isNull } from "lodash";
import React, { useState, useEffect } from "react";
import PropTypes from "prop-types";
import Checkbox from "antd/lib/checkbox";
import Modal from "antd/lib/modal";
import Form from "antd/lib/form";
import Button from "antd/lib/button";
import Select from "antd/lib/select";
import Input from "antd/lib/input";
import Divider from "antd/lib/divider";
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
import QuerySelector from "@/components/QuerySelector";
import { Query } from "@/services/query";
const { Option } = Select;
const formItemProps = { labelCol: { span: 6 }, wrapperCol: { span: 16 } };
function getDefaultTitle(text) {
return capitalize(words(text).join(' ')); // humanize
return capitalize(words(text).join(" ")); // humanize
}
function isTypeDateRange(type) {
@@ -26,30 +25,26 @@ function isTypeDateRange(type) {
function joinExampleList(multiValuesOptions) {
const { prefix, suffix } = multiValuesOptions;
return ['value1', 'value2', 'value3']
.map(value => `${prefix}${value}${suffix}`)
.join(',');
return ["value1", "value2", "value3"].map(value => `${prefix}${value}${suffix}`).join(",");
}
function NameInput({ name, type, onChange, existingNames, setValidation }) {
let helpText = '';
let validateStatus = '';
let helpText = "";
let validateStatus = "";
if (!name) {
helpText = 'Choose a keyword for this parameter';
helpText = "Choose a keyword for this parameter";
setValidation(false);
} else if (includes(existingNames, name)) {
helpText = 'Parameter with this name already exists';
helpText = "Parameter with this name already exists";
setValidation(false);
validateStatus = 'error';
validateStatus = "error";
} else {
if (isTypeDateRange(type)) {
helpText = (
<React.Fragment>
Appears in query as {' '}
<code style={{ display: 'inline-block', color: 'inherit' }}>
{`{{${name}.start}} {{${name}.end}}`}
</code>
Appears in query as{" "}
<code style={{ display: "inline-block", color: "inherit" }}>{`{{${name}.start}} {{${name}.end}}`}</code>
</React.Fragment>
);
}
@@ -57,13 +52,7 @@ function NameInput({ name, type, onChange, existingNames, setValidation }) {
}
return (
<Form.Item
required
label="Keyword"
help={helpText}
validateStatus={validateStatus}
{...formItemProps}
>
<Form.Item required label="Keyword" help={helpText} validateStatus={validateStatus} {...formItemProps}>
<Input onChange={e => onChange(e.target.value)} autoFocus />
</Form.Item>
);
@@ -86,13 +75,11 @@ function EditParameterSettingsDialog(props) {
// fetch query by id
useEffect(() => {
const { queryId } = props.parameter;
const queryId = props.parameter.queryId;
if (queryId) {
Query.get({ id: queryId }, (query) => {
setInitialQuery(query);
});
Query.get({ id: queryId }).then(setInitialQuery);
}
}, []);
}, [props.parameter.queryId]);
function isFulfilled() {
// name
@@ -101,12 +88,12 @@ function EditParameterSettingsDialog(props) {
}
// title
if (param.title === '') {
if (param.title === "") {
return false;
}
// query
if (param.type === 'query' && !param.queryId) {
if (param.type === "query" && !param.queryId) {
return false;
}
@@ -129,16 +116,22 @@ function EditParameterSettingsDialog(props) {
return (
<Modal
{...props.dialog.props}
title={isNew ? 'Add Parameter' : param.name}
title={isNew ? "Add Parameter" : param.name}
width={600}
footer={[(
<Button key="cancel" onClick={props.dialog.dismiss}>Cancel</Button>
), (
<Button key="submit" htmlType="submit" disabled={!isFulfilled()} type="primary" form="paramForm" data-test="SaveParameterSettings">
{isNew ? 'Add Parameter' : 'OK'}
</Button>
)]}
>
footer={[
<Button key="cancel" onClick={props.dialog.dismiss}>
Cancel
</Button>,
<Button
key="submit"
htmlType="submit"
disabled={!isFulfilled()}
type="primary"
form="paramForm"
data-test="SaveParameterSettings">
{isNew ? "Add Parameter" : "OK"}
</Button>,
]}>
<Form layout="horizontal" onSubmit={onConfirm} id="paramForm">
{isNew && (
<NameInput
@@ -158,25 +151,35 @@ function EditParameterSettingsDialog(props) {
</Form.Item>
<Form.Item label="Type" {...formItemProps}>
<Select value={param.type} onChange={type => setParam({ ...param, type })} data-test="ParameterTypeSelect">
<Option value="text" data-test="TextParameterTypeOption">Text</Option>
<Option value="number" data-test="NumberParameterTypeOption">Number</Option>
<Option value="text" data-test="TextParameterTypeOption">
Text
</Option>
<Option value="number" data-test="NumberParameterTypeOption">
Number
</Option>
<Option value="enum">Dropdown List</Option>
<Option value="query">Query Based Dropdown List</Option>
<Option disabled key="dv1">
<Divider className="select-option-divider" />
</Option>
<Option value="date" data-test="DateParameterTypeOption">Date</Option>
<Option value="datetime-local" data-test="DateTimeParameterTypeOption">Date and Time</Option>
<Option value="date" data-test="DateParameterTypeOption">
Date
</Option>
<Option value="datetime-local" data-test="DateTimeParameterTypeOption">
Date and Time
</Option>
<Option value="datetime-with-seconds">Date and Time (with seconds)</Option>
<Option disabled key="dv2">
<Divider className="select-option-divider" />
</Option>
<Option value="date-range" data-test="DateRangeParameterTypeOption">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-with-seconds">Date and Time Range (with seconds)</Option>
</Select>
</Form.Item>
{param.type === 'enum' && (
{param.type === "enum" && (
<Form.Item label="Values" help="Dropdown list values (newline delimited)" {...formItemProps}>
<Input.TextArea
rows={3}
@@ -185,7 +188,7 @@ function EditParameterSettingsDialog(props) {
/>
</Form.Item>
)}
{param.type === 'query' && (
{param.type === "query" && (
<Form.Item label="Query" help="Select query to load dropdown values from" {...formItemProps}>
<QuerySelector
selectedQuery={initialQuery}
@@ -194,45 +197,54 @@ function EditParameterSettingsDialog(props) {
/>
</Form.Item>
)}
{(param.type === 'enum' || param.type === 'query') && (
{(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
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 && (
{(param.type === "enum" || param.type === "query") && param.multiValuesOptions && (
<Form.Item
label="Quotation"
help={(
help={
<React.Fragment>
Placed in query as: <code>{joinExampleList(param.multiValuesOptions)}</code>
</React.Fragment>
)}
{...formItemProps}
>
}
{...formItemProps}>
<Select
value={param.multiValuesOptions.prefix}
onChange={quoteOption => setParam({ ...param,
multiValuesOptions: {
...param.multiValuesOptions,
prefix: quoteOption,
suffix: quoteOption,
} })}
data-test="QuotationSelect"
>
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>
<Option value={'"'} data-test="DoubleQuotationMarkOption">
Double Quotation Mark
</Option>
</Select>
</Form.Item>
)}

View File

@@ -1,15 +1,13 @@
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 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 QueryResultsLink from './QueryResultsLink';
import QueryResultsLink from "./QueryResultsLink";
export function QueryControlDropdown(props) {
export default function QueryControlDropdown(props) {
const menu = (
<Menu>
{!props.query.isNew() && (!props.query.is_draft || !props.query.is_archived) && (
@@ -28,15 +26,26 @@ export function QueryControlDropdown(props) {
)}
<Menu.Item>
<QueryResultsLink
fileType="csv"
disabled={props.queryExecuting || !props.queryResult.getData || !props.queryResult.getData()}
query={props.query}
queryResult={props.queryResult}
embed={props.embed}
apiKey={props.apiKey}
>
apiKey={props.apiKey}>
<Icon type="file" /> Download as CSV File
</QueryResultsLink>
</Menu.Item>
<Menu.Item>
<QueryResultsLink
fileType="tsv"
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 TSV File
</QueryResultsLink>
</Menu.Item>
<Menu.Item>
<QueryResultsLink
fileType="xlsx"
@@ -44,8 +53,7 @@ export function QueryControlDropdown(props) {
query={props.query}
queryResult={props.queryResult}
embed={props.embed}
apiKey={props.apiKey}
>
apiKey={props.apiKey}>
<Icon type="file-excel" /> Download as Excel File
</QueryResultsLink>
</Menu.Item>
@@ -53,11 +61,7 @@ export function QueryControlDropdown(props) {
);
return (
<Dropdown
trigger={['click']}
overlay={menu}
overlayClassName="query-control-dropdown-overlay"
>
<Dropdown trigger={["click"]} overlay={menu} overlayClassName="query-control-dropdown-overlay">
<Button data-test="QueryControlDropdownButton">
<Icon type="ellipsis" rotate={90} />
</Button>
@@ -72,22 +76,13 @@ QueryControlDropdown.propTypes = {
showEmbedDialog: PropTypes.func.isRequired,
embed: PropTypes.bool,
apiKey: PropTypes.string,
selectedTab: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]),
selectedTab: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
openAddToDashboardForm: PropTypes.func.isRequired,
};
QueryControlDropdown.defaultProps = {
queryResult: {},
embed: false,
apiKey: '',
selectedTab: '',
apiKey: "",
selectedTab: "",
};
export default function init(ngModule) {
ngModule.component('queryControlDropdown', react2angular(QueryControlDropdown));
}
init.init = true;

View File

@@ -1,9 +1,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import React from "react";
import PropTypes from "prop-types";
export default function QueryResultsLink(props) {
let href = '';
let href = "";
const { query, queryResult, fileType } = props;
const resultId = queryResult.getId && queryResult.getId();
@@ -11,9 +10,7 @@ export default function QueryResultsLink(props) {
if (resultId && resultData && query.name) {
if (query.id) {
href = `api/queries/${query.id}/results/${resultId}.${fileType}${
props.embed ? `?api_key=${props.apiKey}` : ''
}`;
href = `api/queries/${query.id}/results/${resultId}.${fileType}${props.embed ? `?api_key=${props.apiKey}` : ""}`;
} else {
href = `api/query_results/${resultId}.${fileType}`;
}
@@ -33,15 +30,12 @@ QueryResultsLink.propTypes = {
disabled: PropTypes.bool.isRequired,
embed: PropTypes.bool,
apiKey: PropTypes.string,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
]).isRequired,
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired,
};
QueryResultsLink.defaultProps = {
queryResult: {},
fileType: 'csv',
fileType: "csv",
embed: false,
apiKey: '',
apiKey: "",
};

View File

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

View File

@@ -1,10 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import { clientConfig, currentUser } from '@/services/auth';
import Tooltip from 'antd/lib/tooltip';
import Alert from 'antd/lib/alert';
import HelpTrigger from '@/components/HelpTrigger';
import React from "react";
import PropTypes from "prop-types";
import cx from "classnames";
import { clientConfig, currentUser } from "@/services/auth";
import Tooltip from "antd/lib/tooltip";
import Alert from "antd/lib/alert";
import HelpTrigger from "@/components/HelpTrigger";
export default function EmailSettingsWarning({ featureName, className, mode, adminOnly }) {
if (!clientConfig.mailSettingsMissing) {
@@ -17,33 +17,31 @@ export default function EmailSettingsWarning({ featureName, className, mode, adm
const message = (
<span>
Your mail server isn&apos;t configured correctly, and is needed for {featureName} to work.{' '}
Your mail server isn&apos;t configured correctly, and is needed for {featureName} to work.{" "}
<HelpTrigger type="MAIL_CONFIG" className="f-inherit" />
</span>
);
if (mode === 'icon') {
if (mode === "icon") {
return (
<Tooltip title={message}>
<i className={cx('fa fa-exclamation-triangle', className)} />
<i className={cx("fa fa-exclamation-triangle", className)} />
</Tooltip>
);
}
return (
<Alert message={message} type="error" className={className} />
);
return <Alert message={message} type="error" className={className} />;
}
EmailSettingsWarning.propTypes = {
featureName: PropTypes.string.isRequired,
className: PropTypes.string,
mode: PropTypes.oneOf(['alert', 'icon']),
mode: PropTypes.oneOf(["alert", "icon"]),
adminOnly: PropTypes.bool,
};
EmailSettingsWarning.defaultProps = {
className: null,
mode: 'alert',
mode: "alert",
adminOnly: false,
};

View File

@@ -0,0 +1,74 @@
import { isFunction } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import debug from "debug";
import Alert from "antd/lib/alert";
const logger = debug("redash:errors");
export const ErrorBoundaryContext = React.createContext({
handleError: error => {
// Allow calling chain to roll up, and then throw the error in global context
setTimeout(() => {
throw error;
});
},
reset: () => {},
});
export function ErrorMessage({ children }) {
return <Alert message={children} type="error" showIcon />;
}
ErrorMessage.propTypes = {
children: PropTypes.node,
};
ErrorMessage.defaultProps = {
children: "Something went wrong.",
};
export default class ErrorBoundary extends React.Component {
static propTypes = {
children: PropTypes.node,
renderError: PropTypes.func, // error => ReactNode
};
static defaultProps = {
children: null,
renderError: null,
};
state = { error: null };
handleError = error => {
this.setState(this.constructor.getDerivedStateFromError(error));
this.componentDidCatch(error, null);
};
reset = () => {
this.setState({ error: null });
};
static getDerivedStateFromError(error) {
return { error };
}
componentDidCatch(error, errorInfo) {
logger(error, errorInfo);
}
render() {
const { renderError, children } = this.props;
const { error } = this.state;
if (error) {
if (isFunction(renderError)) {
return renderError(error);
}
return <ErrorMessage />;
}
return <ErrorBoundaryContext.Provider value={this}>{children}</ErrorBoundaryContext.Provider>;
}
}

View File

@@ -1,78 +1,40 @@
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import { $rootScope } from '@/services/ng';
import React from "react";
import PropTypes from "prop-types";
export class FavoritesControl extends React.Component {
export default class FavoritesControl extends React.Component {
static propTypes = {
item: PropTypes.shape({
is_favorite: PropTypes.bool.isRequired,
}).isRequired,
onChange: PropTypes.func,
// Force component update when `item` changes.
// Remove this when `react2angular` will finally go to hell
forceUpdate: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
};
static defaultProps = {
onChange: () => {},
forceUpdate: '',
};
toggleItem(event, item, callback) {
const action = item.is_favorite ? item.$unfavorite.bind(item) : item.$favorite.bind(item);
const action = item.is_favorite ? item.unfavorite.bind(item) : item.favorite.bind(item);
const savedIsFavorite = item.is_favorite;
action().then(() => {
item.is_favorite = !savedIsFavorite;
this.forceUpdate();
$rootScope.$broadcast('reloadFavorites');
callback();
});
}
render() {
const { item, onChange } = this.props;
const icon = item.is_favorite ? 'fa fa-star' : 'fa fa-star-o';
const title = item.is_favorite ? 'Remove from favorites' : 'Add to favorites';
const icon = item.is_favorite ? "fa fa-star" : "fa fa-star-o";
const title = item.is_favorite ? "Remove from favorites" : "Add to favorites";
return (
<a
title={title}
className="btn-favourite"
onClick={event => this.toggleItem(event, item, onChange)}
>
className="favorites-control btn-favourite"
onClick={event => this.toggleItem(event, item, onChange)}>
<i className={icon} aria-hidden="true" />
</a>
);
}
}
export default function init(ngModule) {
ngModule.component('favoritesControlImpl', react2angular(FavoritesControl));
ngModule.component('favoritesControl', {
template: `
<favorites-control-impl
ng-if="$ctrl.item"
item="$ctrl.item"
on-change="$ctrl.onChange"
force-update="$ctrl.forceUpdateTag"
></favorites-control-impl>
`,
bindings: {
item: '=',
},
controller($scope) {
// See comment for FavoritesControl.propTypes.forceUpdate
this.forceUpdateTag = 'force' + Date.now();
$scope.$on('reloadFavorites', () => {
this.forceUpdateTag = 'force' + Date.now();
});
this.onChange = () => {
$scope.$applyAsync();
};
},
});
}
init.init = true;

View File

@@ -1,22 +1,18 @@
import { isArray, indexOf, get, map, includes, every, some, toNumber } 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 { formatColumnValue } from '@/filters';
import { isArray, indexOf, get, map, includes, every, some, toNumber } from "lodash";
import moment from "moment";
import React from "react";
import PropTypes from "prop-types";
import Select from "antd/lib/select";
import { formatColumnValue } from "@/lib/utils";
const ALL_VALUES = '###Redash::Filters::SelectAll###';
const NONE_VALUES = '###Redash::Filters::Clear###';
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),
]),
current: PropTypes.oneOfType([PropTypes.any, PropTypes.arrayOf(PropTypes.any)]),
values: PropTypes.arrayOf(PropTypes.any).isRequired,
});
@@ -49,29 +45,28 @@ export function filterData(rows, filters = []) {
let result = rows;
if (isArray(filters) && (filters.length > 0)) {
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) => {
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) => {
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 filterValue === rowValue || String(rowValue) === filterValue;
});
},
));
})
);
}
return result;
}
export function Filters({ filters, onChange }) {
function Filters({ filters, onChange }) {
if (filters.length === 0) {
return null;
}
@@ -82,36 +77,45 @@ export function Filters({ filters, onChange }) {
<div className="filters-wrapper">
<div className="container bg-white">
<div className="row">
{map(filters, (filter) => {
{map(filters, filter => {
const options = map(filter.values, (value, index) => (
<Select.Option key={index}>{formatColumnValue(value, get(filter, 'column.type'))}</Select.Option>
<Select.Option key={index}>{formatColumnValue(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) && (
{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: formatColumnValue(value) })) :
({ key: `${indexOf(filter.values, filter.current)}`, label: formatColumnValue(filter.current) })}
mode={filter.multiple ? "multiple" : "default"}
value={
isArray(filter.current)
? map(filter.current, value => ({
key: `${indexOf(filter.values, value)}`,
label: formatColumnValue(value),
}))
: { key: `${indexOf(filter.values, filter.current)}`, label: formatColumnValue(filter.current) }
}
allowClear={filter.multiple}
optionFilterProp="children"
showSearch
onChange={values => onChange(filter, values)}
>
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.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>
)}
@@ -133,8 +137,4 @@ Filters.defaultProps = {
onChange: () => {},
};
export default function init(ngModule) {
ngModule.component('filters', react2angular(Filters));
}
init.init = true;
export default Filters;

View File

@@ -1,111 +1,63 @@
import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import Tooltip from 'antd/lib/tooltip';
import Drawer from 'antd/lib/drawer';
import Icon from 'antd/lib/icon';
import { BigMessage } from '@/components/BigMessage';
import DynamicComponent from '@/components/DynamicComponent';
import { startsWith } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import cx from "classnames";
import Tooltip from "antd/lib/tooltip";
import Drawer from "antd/lib/drawer";
import Icon from "antd/lib/icon";
import BigMessage from "@/components/BigMessage";
import DynamicComponent from "@/components/DynamicComponent";
import './HelpTrigger.less';
import "./HelpTrigger.less";
const DOMAIN = 'https://redash.io';
const HELP_PATH = '/help';
const DOMAIN = "https://redash.io";
const HELP_PATH = "/help";
const IFRAME_TIMEOUT = 20000;
const IFRAME_URL_UPDATE_MESSAGE = 'iframe_url';
const IFRAME_URL_UPDATE_MESSAGE = "iframe_url";
export const TYPES = {
HOME: [
'',
'Help',
],
VALUE_SOURCE_OPTIONS: [
'/user-guide/querying/query-parameters#Value-Source-Options',
'Guide: Value Source Options',
],
SHARE_DASHBOARD: [
'/user-guide/dashboards/sharing-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',
],
ALERT_SETUP: [
'/user-guide/alerts/setting-up-an-alert',
'Guide: Setting Up a New Alert',
],
MAIL_CONFIG: [
'/open-source/setup/#Mail-Configuration',
'Guide: Mail Configuration',
],
ALERT_NOTIF_TEMPLATE_GUIDE: [
'/user-guide/alerts/custom-alert-notifications',
'Guide: Custom Alerts Notifications',
],
FAVORITES: [
'/user-guide/querying/favorites-tagging/#Favorites',
'Guide: Favorites',
HOME: ["", "Help"],
VALUE_SOURCE_OPTIONS: ["/user-guide/querying/query-parameters#Value-Source-Options", "Guide: Value Source Options"],
SHARE_DASHBOARD: ["/user-guide/dashboards/sharing-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"],
ALERT_SETUP: ["/user-guide/alerts/setting-up-an-alert", "Guide: Setting Up a New Alert"],
MAIL_CONFIG: ["/open-source/setup/#Mail-Configuration", "Guide: Mail Configuration"],
ALERT_NOTIF_TEMPLATE_GUIDE: ["/user-guide/alerts/custom-alert-notifications", "Guide: Custom Alerts Notifications"],
FAVORITES: ["/user-guide/querying/favorites-tagging/#Favorites", "Guide: Favorites"],
MANAGE_PERMISSIONS: [
"/user-guide/querying/writing-queries#Managing-Query-Permissions",
"Guide: Managing Query Permissions",
],
NUMBER_FORMAT_SPECS: ["/user-guide/visualizations/formatting-numbers", "Formatting Numbers"],
};
export default class HelpTrigger extends React.Component {
static propTypes = {
type: PropTypes.oneOf(Object.keys(TYPES)).isRequired,
className: PropTypes.string,
showTooltip: PropTypes.bool,
children: PropTypes.node,
};
static defaultProps = {
className: null,
showTooltip: true,
children: <i className="fa fa-question-circle" />,
};
iframeRef = null;
iframeRef = React.createRef();
iframeLoadingTimeout = null;
constructor(props) {
super(props);
this.iframeRef = React.createRef();
}
state = {
visible: false,
loading: false,
@@ -114,15 +66,15 @@ export default class HelpTrigger extends React.Component {
};
componentDidMount() {
window.addEventListener('message', this.onPostMessageReceived, DOMAIN);
window.addEventListener("message", this.onPostMessageReceived, false);
}
componentWillUnmount() {
window.removeEventListener('message', this.onPostMessageReceived);
window.removeEventListener("message", this.onPostMessageReceived);
clearTimeout(this.iframeLoadingTimeout);
}
loadIframe = (url) => {
loadIframe = url => {
clearTimeout(this.iframeLoadingTimeout);
this.setState({ loading: true, error: false });
@@ -137,14 +89,18 @@ export default class HelpTrigger extends React.Component {
clearTimeout(this.iframeLoadingTimeout);
};
onPostMessageReceived = (event) => {
onPostMessageReceived = event => {
if (!startsWith(event.origin, DOMAIN)) {
return;
}
const { type, message: currentUrl } = event.data || {};
if (type !== IFRAME_URL_UPDATE_MESSAGE) {
return;
}
this.setState({ currentUrl });
}
};
openDrawer = () => {
this.setState({ visible: true });
@@ -155,7 +111,7 @@ export default class HelpTrigger extends React.Component {
setTimeout(() => this.loadIframe(url), 300);
};
closeDrawer = (event) => {
closeDrawer = event => {
if (event) {
event.preventDefault();
}
@@ -165,12 +121,12 @@ export default class HelpTrigger extends React.Component {
render() {
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 (
<React.Fragment>
<Tooltip title={tooltip}>
<Tooltip title={this.props.showTooltip ? tooltip : null}>
<a onClick={this.openDrawer} className={className}>
{this.props.children}
</a>
@@ -182,8 +138,7 @@ export default class HelpTrigger extends React.Component {
visible={this.state.visible}
className="help-drawer"
destroyOnClose
width={400}
>
width={400}>
<div className="drawer-wrapper">
<div className="drawer-menu">
{url && (
@@ -220,20 +175,19 @@ export default class HelpTrigger extends React.Component {
{/* error message */}
{this.state.error && (
<BigMessage icon="fa-exclamation-circle" className="help-message">
Something went wrong.<br />
Something went wrong.
<br />
{/* eslint-disable-next-line react/jsx-no-target-blank */}
<a href={this.state.error} target="_blank" rel="noopener">Click here</a>{' '}
<a href={this.state.error} target="_blank" rel="noopener">
Click here
</a>{" "}
to open the page in a new window.
</BigMessage>
)}
</div>
{/* extra content */}
<DynamicComponent
name="HelpDrawerExtraContent"
onLeave={this.closeDrawer}
openPageUrl={this.loadIframe}
/>
<DynamicComponent name="HelpDrawerExtraContent" onLeave={this.closeDrawer} openPageUrl={this.loadIframe} />
</Drawer>
</React.Fragment>
);

View File

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

View File

@@ -1,14 +1,14 @@
import React from 'react';
import Input from 'antd/lib/input';
import Icon from 'antd/lib/icon';
import Tooltip from 'antd/lib/tooltip';
import React from "react";
import Input from "antd/lib/input";
import Icon from "antd/lib/icon";
import Tooltip from "antd/lib/tooltip";
export default class InputWithCopy extends React.Component {
constructor(props) {
super(props);
this.state = { copied: null };
this.ref = React.createRef();
this.copyFeatureSupported = document.queryCommandSupported('copy');
this.copyFeatureSupported = document.queryCommandSupported("copy");
this.resetCopyState = null;
}
@@ -24,14 +24,14 @@ export default class InputWithCopy extends React.Component {
// copy
try {
const success = document.execCommand('copy');
const success = document.execCommand("copy");
if (!success) {
throw new Error();
}
this.setState({ copied: 'Copied!' });
this.setState({ copied: "Copied!" });
} catch (err) {
this.setState({
copied: 'Copy failed',
copied: "Copy failed",
});
}
@@ -41,17 +41,11 @@ export default class InputWithCopy extends React.Component {
render() {
const copyButton = (
<Tooltip title={this.state.copied || 'Copy'}>
<Icon
type="copy"
style={{ cursor: 'pointer' }}
onClick={this.copy}
/>
<Tooltip title={this.state.copied || "Copy"}>
<Icon type="copy" style={{ cursor: "pointer" }} onClick={this.copy} />
</Tooltip>
);
return (
<Input {...this.props} ref={this.ref} addonAfter={this.copyFeatureSupported && copyButton} />
);
return <Input {...this.props} ref={this.ref} addonAfter={this.copyFeatureSupported && copyButton} />;
}
}

View File

@@ -1,27 +1,18 @@
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import { BigMessage } from '@/components/BigMessage';
import { TagsControl } from '@/components/tags-control/TagsControl';
import React from "react";
import PropTypes from "prop-types";
import BigMessage from "@/components/BigMessage";
import { TagsControl } from "@/components/tags-control/TagsControl";
export function NoTaggedObjectsFound({ objectType, tags }) {
export default function NoTaggedObjectsFound({ objectType, tags }) {
return (
<BigMessage icon="fa-tags">
No {objectType} found tagged with&nbsp;<TagsControl className="inline-tags-control" tags={Array.from(tags)} />.
No {objectType} found tagged with&nbsp;
<TagsControl className="inline-tags-control" tags={Array.from(tags)} />.
</BigMessage>
);
}
NoTaggedObjectsFound.propTypes = {
objectType: PropTypes.string.isRequired,
tags: PropTypes.oneOfType([
PropTypes.array,
PropTypes.objectOf(Set),
]).isRequired,
tags: PropTypes.oneOfType([PropTypes.array, PropTypes.objectOf(Set)]).isRequired,
};
export default function init(ngModule) {
ngModule.component('noTaggedObjectsFound', react2angular(NoTaggedObjectsFound));
}
init.init = true;

View File

@@ -1,12 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import React from "react";
import PropTypes from "prop-types";
export function PageHeader({ title }) {
export default function PageHeader({ title }) {
return (
<div className="page-header-wrapper row p-l-15 p-r-15 m-b-10 m-l-0 m-r-0">
<div className="col-sm-9 p-l-0 p-r-0">
<h3>{ title }</h3>
<h3>{title}</h3>
</div>
</div>
);
@@ -15,9 +14,3 @@ export function PageHeader({ title }) {
PageHeader.propTypes = {
title: PropTypes.string.isRequired,
};
export default function init(ngModule) {
ngModule.component('pageHeader', react2angular(PageHeader));
}
init.init = true;

View File

@@ -1,25 +1,14 @@
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import Pagination from 'antd/lib/pagination';
import React from "react";
import PropTypes from "prop-types";
import Pagination from "antd/lib/pagination";
export function Paginator({
page,
itemsPerPage,
totalCount,
onChange,
}) {
export default function Paginator({ page, itemsPerPage, totalCount, onChange }) {
if (totalCount <= itemsPerPage) {
return null;
}
return (
<div className="paginator-container">
<Pagination
defaultCurrent={page}
defaultPageSize={itemsPerPage}
total={totalCount}
onChange={onChange}
/>
<Pagination defaultCurrent={page} defaultPageSize={itemsPerPage} total={totalCount} onChange={onChange} />
</div>
);
}
@@ -34,27 +23,3 @@ Paginator.propTypes = {
Paginator.defaultProps = {
onChange: () => {},
};
export default function init(ngModule) {
ngModule.component('paginatorImpl', react2angular(Paginator));
ngModule.component('paginator', {
template: `
<paginator-impl
page="$ctrl.paginator.page"
items-per-page="$ctrl.paginator.itemsPerPage"
total-count="$ctrl.paginator.totalCount"
on-change="$ctrl.onPageChanged"
></paginator-impl>`,
bindings: {
paginator: '<',
},
controller($scope) {
this.onPageChanged = (page) => {
this.paginator.setPage(page);
$scope.$applyAsync();
};
},
});
}
init.init = true;

View File

@@ -1,18 +1,18 @@
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';
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/KeyboardShortcuts";
function ParameterApplyButton({ paramCount, onClick }) {
// show spinner when count is empty so the fade out is consistent
const icon = !paramCount ? 'spinner fa-pulse' : 'check';
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`}>
<Tooltip title={paramCount ? `${KeyboardShortcuts.modKey} + Enter` : null}>
<span>
<Button onClick={onClick}>
<i className={`fa fa-${icon}`} /> Apply Changes

View File

@@ -1,38 +1,37 @@
/* eslint-disable react/no-multi-comp */
import { isString, extend, each, has, map, includes, findIndex, find,
fromPairs, clone, isEmpty } from 'lodash';
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import Select from 'antd/lib/select';
import Table from 'antd/lib/table';
import Popover from 'antd/lib/popover';
import Button from 'antd/lib/button';
import Icon from 'antd/lib/icon';
import Tag from 'antd/lib/tag';
import Input from 'antd/lib/input';
import Radio from 'antd/lib/radio';
import Form from 'antd/lib/form';
import Tooltip from 'antd/lib/tooltip';
import ParameterValueInput from '@/components/ParameterValueInput';
import { ParameterMappingType } from '@/services/widget';
import { Parameter } from '@/services/parameters';
import HelpTrigger from '@/components/HelpTrigger';
import { isString, extend, each, has, map, includes, findIndex, find, fromPairs, clone, isEmpty } from "lodash";
import React, { Fragment } from "react";
import PropTypes from "prop-types";
import classNames from "classnames";
import Select from "antd/lib/select";
import Table from "antd/lib/table";
import Popover from "antd/lib/popover";
import Button from "antd/lib/button";
import Icon from "antd/lib/icon";
import Tag from "antd/lib/tag";
import Input from "antd/lib/input";
import Radio from "antd/lib/radio";
import Form from "antd/lib/form";
import Tooltip from "antd/lib/tooltip";
import ParameterValueInput from "@/components/ParameterValueInput";
import { ParameterMappingType } from "@/services/widget";
import { Parameter, cloneParameter } from "@/services/parameters";
import HelpTrigger from "@/components/HelpTrigger";
import './ParameterMappingInput.less';
import "./ParameterMappingInput.less";
const { Option } = Select;
export const MappingType = {
DashboardAddNew: 'dashboard-add-new',
DashboardMapToExisting: 'dashboard-map-to-existing',
WidgetLevel: 'widget-level',
StaticValue: 'static-value',
DashboardAddNew: "dashboard-add-new",
DashboardMapToExisting: "dashboard-map-to-existing",
WidgetLevel: "widget-level",
StaticValue: "static-value",
};
export function parameterMappingsToEditableMappings(mappings, parameters, existingParameterNames = []) {
return map(mappings, (mapping) => {
return map(mappings, mapping => {
const result = extend({}, mapping);
const alreadyExists = includes(existingParameterNames, mapping.mapTo);
result.param = find(parameters, p => p.name === mapping.name);
@@ -43,7 +42,7 @@ export function parameterMappingsToEditableMappings(mappings, parameters, existi
break;
case ParameterMappingType.StaticValue:
result.type = MappingType.StaticValue;
result.param = result.param.clone();
result.param = cloneParameter(result.param);
result.param.setValue(result.value);
break;
case ParameterMappingType.WidgetLevel:
@@ -57,49 +56,52 @@ export function parameterMappingsToEditableMappings(mappings, parameters, existi
}
export function editableMappingsToParameterMappings(mappings) {
return fromPairs(map( // convert to map
mappings,
(mapping) => {
const result = extend({}, mapping);
switch (mapping.type) {
case MappingType.DashboardAddNew:
result.type = ParameterMappingType.DashboardLevel;
result.value = null;
break;
case MappingType.DashboardMapToExisting:
result.type = ParameterMappingType.DashboardLevel;
result.value = null;
break;
case MappingType.StaticValue:
result.type = ParameterMappingType.StaticValue;
result.param = mapping.param.clone();
result.param.setValue(result.value);
result.value = result.param.value;
break;
case MappingType.WidgetLevel:
result.type = ParameterMappingType.WidgetLevel;
result.value = null;
break;
// no default
return fromPairs(
map(
// convert to map
mappings,
mapping => {
const result = extend({}, mapping);
switch (mapping.type) {
case MappingType.DashboardAddNew:
result.type = ParameterMappingType.DashboardLevel;
result.value = null;
break;
case MappingType.DashboardMapToExisting:
result.type = ParameterMappingType.DashboardLevel;
result.value = null;
break;
case MappingType.StaticValue:
result.type = ParameterMappingType.StaticValue;
result.param = cloneParameter(mapping.param);
result.param.setValue(result.value);
result.value = result.param.value;
break;
case MappingType.WidgetLevel:
result.type = ParameterMappingType.WidgetLevel;
result.value = null;
break;
// no default
}
delete result.param;
return [result.name, result];
}
delete result.param;
return [result.name, result];
},
));
)
);
}
export function synchronizeWidgetTitles(sourceMappings, widgets) {
const affectedWidgets = [];
each(sourceMappings, (sourceMapping) => {
each(sourceMappings, sourceMapping => {
if (sourceMapping.type === ParameterMappingType.DashboardLevel) {
each(widgets, (widget) => {
each(widgets, widget => {
const widgetMappings = widget.options.parameterMappings;
each(widgetMappings, (widgetMapping) => {
each(widgetMappings, widgetMapping => {
// check if mapped to the same dashboard-level parameter
if (
(widgetMapping.type === ParameterMappingType.DashboardLevel) &&
(widgetMapping.mapTo === sourceMapping.mapTo)
widgetMapping.type === ParameterMappingType.DashboardLevel &&
widgetMapping.mapTo === sourceMapping.mapTo
) {
// dirty check - update only when needed
if (widgetMapping.title !== sourceMapping.title) {
@@ -133,33 +135,32 @@ export class ParameterMappingInput extends React.Component {
formItemProps = {
labelCol: { span: 5 },
wrapperCol: { span: 16 },
className: 'form-item',
className: "form-item",
};
updateSourceType = (type) => {
let { mapping: { mapTo } } = this.props;
updateSourceType = type => {
let {
mapping: { mapTo },
} = this.props;
const { existingParamNames } = this.props;
// if mapped name doesn't already exists
// default to first select option
if (
type === MappingType.DashboardMapToExisting &&
!includes(existingParamNames, mapTo)
) {
if (type === MappingType.DashboardMapToExisting && !includes(existingParamNames, mapTo)) {
mapTo = existingParamNames[0];
}
this.updateParamMapping({ type, mapTo });
};
updateParamMapping = (update) => {
updateParamMapping = update => {
const { onChange, mapping } = this.props;
const newMapping = extend({}, mapping, update);
if (newMapping.value !== mapping.value) {
newMapping.param = newMapping.param.clone();
newMapping.param = cloneParameter(newMapping.param);
newMapping.param.setValue(newMapping.value);
}
if (has(update, 'type')) {
if (has(update, "type")) {
if (update.type === MappingType.StaticValue) {
newMapping.value = newMapping.param.value;
} else {
@@ -172,24 +173,17 @@ export class ParameterMappingInput extends React.Component {
renderMappingTypeSelector() {
const noExisting = isEmpty(this.props.existingParamNames);
return (
<Radio.Group
value={this.props.mapping.type}
onChange={e => this.updateSourceType(e.target.value)}
>
<Radio.Group value={this.props.mapping.type} onChange={e => this.updateSourceType(e.target.value)}>
<Radio className="radio" value={MappingType.DashboardAddNew} data-test="NewDashboardParameterOption">
New dashboard parameter
</Radio>
<Radio
className="radio"
value={MappingType.DashboardMapToExisting}
disabled={noExisting}
>
Existing dashboard parameter{' '}
<Radio className="radio" value={MappingType.DashboardMapToExisting} disabled={noExisting}>
Existing dashboard parameter{" "}
{noExisting ? (
<Tooltip title="There are no dashboard parameters corresponding to this data type">
<Icon type="question-circle" theme="filled" />
</Tooltip>
) : null }
) : null}
</Radio>
<Radio className="radio" value={MappingType.WidgetLevel} data-test="WidgetParameterOption">
Widget parameter
@@ -202,13 +196,10 @@ export class ParameterMappingInput extends React.Component {
}
renderDashboardAddNew() {
const { mapping: { mapTo } } = this.props;
return (
<Input
value={mapTo}
onChange={e => this.updateParamMapping({ mapTo: e.target.value })}
/>
);
const {
mapping: { mapTo },
} = this.props;
return <Input value={mapTo} onChange={e => this.updateParamMapping({ mapTo: e.target.value })} />;
}
renderDashboardMapToExisting() {
@@ -218,10 +209,11 @@ export class ParameterMappingInput extends React.Component {
<Select
value={mapping.mapTo}
onChange={mapTo => this.updateParamMapping({ mapTo })}
dropdownMatchSelectWidth={false}
>
dropdownMatchSelectWidth={false}>
{map(existingParamNames, name => (
<Option value={name} key={name}>{ name }</Option>
<Option value={name} key={name}>
{name}
</Option>
))}
</Select>
);
@@ -245,24 +237,13 @@ export class ParameterMappingInput extends React.Component {
const { mapping } = this.props;
switch (mapping.type) {
case MappingType.DashboardAddNew:
return [
'Key',
'Enter a new parameter keyword',
this.renderDashboardAddNew(),
];
return ["Key", "Enter a new parameter keyword", this.renderDashboardAddNew()];
case MappingType.DashboardMapToExisting:
return [
'Key',
'Select from a list of existing parameters',
this.renderDashboardMapToExisting(),
];
return ["Key", "Select from a list of existing parameters", this.renderDashboardMapToExisting()];
case MappingType.StaticValue:
return [
'Value',
null,
this.renderStaticValue(),
];
default: return [];
return ["Value", null, this.renderStaticValue()];
default:
return [];
}
}
@@ -276,10 +257,10 @@ export class ParameterMappingInput extends React.Component {
{this.renderMappingTypeSelector()}
</Form.Item>
<Form.Item
style={{ height: 60, visibility: input ? 'visible' : 'hidden' }}
style={{ height: 60, visibility: input ? "visible" : "hidden" }}
label={label}
{...this.formItemProps}
validateStatus={inputError ? 'error' : ''}
validateStatus={inputError ? "error" : ""}
help={inputError || help} // empty space so line doesn't collapse
>
{input}
@@ -305,18 +286,19 @@ class MappingEditor extends React.Component {
};
}
onVisibleChange = (visible) => {
if (visible) this.show(); else this.hide();
onVisibleChange = visible => {
if (visible) this.show();
else this.hide();
};
onChange = (mapping) => {
onChange = mapping => {
let inputError = null;
if (mapping.type === MappingType.DashboardAddNew) {
if (isEmpty(mapping.mapTo)) {
inputError = 'Keyword must have a value';
inputError = "Keyword must have a value";
} else if (includes(this.props.existingParamNames, mapping.mapTo)) {
inputError = 'A parameter with this name already exists';
inputError = "A parameter with this name already exists";
}
}
@@ -355,7 +337,9 @@ class MappingEditor extends React.Component {
/>
<footer>
<Button onClick={this.hide}>Cancel</Button>
<Button onClick={this.save} disabled={!!inputError} type="primary">OK</Button>
<Button onClick={this.save} disabled={!!inputError} type="primary">
OK
</Button>
</footer>
</div>
);
@@ -369,8 +353,7 @@ class MappingEditor extends React.Component {
trigger="click"
content={this.renderContent()}
visible={visible}
onVisibleChange={this.onVisibleChange}
>
onVisibleChange={this.onVisibleChange}>
<Button size="small" type="dashed" data-test={`EditParamMappingButon-${mapping.param.name}`}>
<Icon type="edit" />
</Button>
@@ -392,24 +375,24 @@ class TitleEditor extends React.Component {
state = {
showPopup: false,
title: '', // will be set on editing
title: "", // will be set on editing
};
onPopupVisibleChange = (showPopup) => {
onPopupVisibleChange = showPopup => {
this.setState({
showPopup,
title: showPopup ? this.getMappingTitle() : '',
title: showPopup ? this.getMappingTitle() : "",
});
};
onEditingTitleChange = (event) => {
onEditingTitleChange = event => {
this.setState({ title: event.target.value });
};
getMappingTitle() {
let { mapping } = this.props;
if (isString(mapping.title) && (mapping.title !== '')) {
if (isString(mapping.title) && mapping.title !== "") {
return mapping.title;
}
@@ -435,7 +418,9 @@ class TitleEditor extends React.Component {
};
renderPopover() {
const { param: { title: paramTitle } } = this.props.mapping;
const {
param: { title: paramTitle },
} = this.props.mapping;
return (
<div className="parameter-mapping-title-editor">
@@ -473,8 +458,7 @@ class TitleEditor extends React.Component {
trigger="click"
content={this.renderPopover()}
visible={this.state.showPopup}
onVisibleChange={this.onPopupVisibleChange}
>
onVisibleChange={this.onPopupVisibleChange}>
<Button size="small" type="dashed">
<Icon type="edit" />
</Button>
@@ -488,7 +472,7 @@ class TitleEditor extends React.Component {
const disabled = mapping.type === MappingType.StaticValue;
return (
<div className={classNames('parameter-mapping-title', { disabled })}>
<div className={classNames("parameter-mapping-title", { disabled })}>
<span className="text">{this.getMappingTitle()}</span>
{this.renderEditButton()}
</div>
@@ -512,17 +496,17 @@ export class ParameterMappingListInput extends React.Component {
static getStringValue(value) {
// null
if (!value) {
return '';
return "";
}
// range
if (value instanceof Object && 'start' in value && 'end' in value) {
if (value instanceof Object && "start" in value && "end" in value) {
return `${value.start} ~ ${value.end}`;
}
// just to be safe, array or object
if (typeof value === 'object') {
return map(value, v => this.getStringValue(v)).join(', ');
if (typeof value === "object") {
return map(value, v => this.getStringValue(v)).join(", ");
}
// rest
@@ -536,13 +520,14 @@ export class ParameterMappingListInput extends React.Component {
// if mapped to another param, swap 'em
if (type === MappingType.DashboardMapToExisting && mapTo !== name) {
const mappedTo = find(existingParams, { name: mapTo });
if (mappedTo) { // just being safe
if (mappedTo) {
// just being safe
param = mappedTo;
}
// static type is different since it's fed param.normalizedValue
// static type is different since it's fed param.normalizedValue
} else if (type === MappingType.StaticValue) {
param = param.clone().setValue(mapping.value);
param = cloneParameter(param).setValue(mapping.value);
}
let value = Parameter.getExecutionValue(param);
@@ -561,16 +546,15 @@ export class ParameterMappingListInput extends React.Component {
case MappingType.DashboardMapToExisting:
return (
<Fragment>
Dashboard{' '}
<Tag className="tag">{mapTo}</Tag>
Dashboard <Tag className="tag">{mapTo}</Tag>
</Fragment>
);
case MappingType.WidgetLevel:
return 'Widget parameter';
return "Widget parameter";
case MappingType.StaticValue:
return 'Static value';
return "Static value";
default:
return ''; // won't happen (typescript-ftw)
return ""; // won't happen (typescript-ftw)
}
}
@@ -592,12 +576,7 @@ export class ParameterMappingListInput extends React.Component {
return (
<div className="parameters-mapping-list">
<Table
dataSource={dataSource}
size="middle"
pagination={false}
rowKey={(record, idx) => `row${idx}`}
>
<Table dataSource={dataSource} size="middle" pagination={false} rowKey={(record, idx) => `row${idx}`}>
<Table.Column
title="Title"
dataIndex="mapping"
@@ -621,22 +600,20 @@ export class ParameterMappingListInput extends React.Component {
title="Default Value"
dataIndex="mapping"
key="value"
render={mapping => (
this.constructor.getDefaultValue(mapping, this.props.existingParams)
)}
render={mapping => this.constructor.getDefaultValue(mapping, this.props.existingParams)}
/>
<Table.Column
title="Value Source"
dataIndex="mapping"
key="source"
render={(mapping) => {
render={mapping => {
const existingParamsNames = existingParams
.filter(({ type }) => type === mapping.param.type) // exclude mismatching param types
.map(({ name }) => name); // keep names only
return (
<Fragment>
{this.constructor.getSourceTypeLabel(mapping)}{' '}
{this.constructor.getSourceTypeLabel(mapping)}{" "}
<MappingEditor
mapping={mapping}
existingParamNames={existingParamsNames}

View File

@@ -1,14 +1,14 @@
import React from 'react';
import PropTypes from 'prop-types';
import Select from 'antd/lib/select';
import Input from 'antd/lib/input';
import InputNumber from 'antd/lib/input-number';
import DateParameter from '@/components/dynamic-parameters/DateParameter';
import DateRangeParameter from '@/components/dynamic-parameters/DateRangeParameter';
import { isEqual } from 'lodash';
import { QueryBasedParameterInput } from './QueryBasedParameterInput';
import { isEqual } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import Select from "antd/lib/select";
import Input from "antd/lib/input";
import InputNumber from "antd/lib/input-number";
import DateParameter from "@/components/dynamic-parameters/DateParameter";
import DateRangeParameter from "@/components/dynamic-parameters/DateRangeParameter";
import QueryBasedParameterInput from "./QueryBasedParameterInput";
import './ParameterValueInput.less';
import "./ParameterValueInput.less";
const { Option } = Select;
@@ -30,13 +30,13 @@ class ParameterValueInput extends React.Component {
};
static defaultProps = {
type: 'text',
type: "text",
value: null,
enumOptions: '',
enumOptions: "",
queryId: null,
parameter: null,
onSelect: () => {},
className: '',
className: "",
};
constructor(props) {
@@ -47,7 +47,7 @@ class ParameterValueInput extends React.Component {
};
}
componentDidUpdate = (prevProps) => {
componentDidUpdate = prevProps => {
const { value, parameter } = this.props;
// if value prop updated, reset dirty state
if (prevProps.value !== value || prevProps.parameter !== parameter) {
@@ -56,13 +56,13 @@ class ParameterValueInput extends React.Component {
isDirty: parameter.hasPendingValue,
});
}
}
};
onSelect = (value) => {
onSelect = value => {
const isDirty = !isEqual(value, this.props.value);
this.setState({ value, isDirty });
this.props.onSelect(value, isDirty);
}
};
renderDateParameter() {
const { type, parameter } = this.props;
@@ -95,13 +95,13 @@ class ParameterValueInput extends React.Component {
renderEnumInput() {
const { enumOptions, parameter } = this.props;
const { value } = this.state;
const enumOptionsArray = enumOptions.split('\n').filter(v => v !== '');
const enumOptionsArray = enumOptions.split("\n").filter(v => v !== "");
// Antd Select doesn't handle null in multiple mode
const normalize = val => (parameter.multiValuesOptions && val === null ? [] : val);
return (
<Select
className={this.props.className}
mode={parameter.multiValuesOptions ? 'multiple' : 'default'}
mode={parameter.multiValuesOptions ? "multiple" : "default"}
optionFilterProp="children"
disabled={enumOptionsArray.length === 0}
value={normalize(value)}
@@ -111,9 +111,12 @@ class ParameterValueInput extends React.Component {
showArrow
style={{ minWidth: 60 }}
notFoundContent={null}
{...multipleValuesProps}
>
{enumOptionsArray.map(option => (<Option key={option} value={option}>{ option }</Option>))}
{...multipleValuesProps}>
{enumOptionsArray.map(option => (
<Option key={option} value={option}>
{option}
</Option>
))}
</Select>
);
}
@@ -124,7 +127,7 @@ class ParameterValueInput extends React.Component {
return (
<QueryBasedParameterInput
className={this.props.className}
mode={parameter.multiValuesOptions ? 'multiple' : 'default'}
mode={parameter.multiValuesOptions ? "multiple" : "default"}
optionFilterProp="children"
parameter={parameter}
value={value}
@@ -143,11 +146,7 @@ class ParameterValueInput extends React.Component {
const normalize = val => (isNaN(val) ? undefined : val);
return (
<InputNumber
className={className}
value={normalize(value)}
onChange={val => this.onSelect(normalize(val))}
/>
<InputNumber className={className} value={normalize(value)} onChange={val => this.onSelect(normalize(val))} />
);
}
@@ -168,16 +167,22 @@ class ParameterValueInput extends React.Component {
renderInput() {
const { type } = this.props;
switch (type) {
case 'datetime-with-seconds':
case 'datetime-local':
case 'date': return this.renderDateParameter();
case 'datetime-range-with-seconds':
case 'datetime-range':
case 'date-range': return this.renderDateRangeParameter();
case 'enum': return this.renderEnumInput();
case 'query': return this.renderQueryBasedInput();
case 'number': return this.renderNumberInput();
default: return this.renderTextInput();
case "datetime-with-seconds":
case "datetime-local":
case "date":
return this.renderDateParameter();
case "datetime-range-with-seconds":
case "datetime-range":
case "date-range":
return this.renderDateRangeParameter();
case "enum":
return this.renderEnumInput();
case "query":
return this.renderQueryBasedInput();
case "number":
return this.renderNumberInput();
default:
return this.renderTextInput();
}
}

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