Compare commits

...

42 Commits

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

* Update version reference.

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

* Fix tests

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

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

This reverts commit 2f346f3bb4.

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

* Update CHANGELOG.md

* Update CHANGELOG.md

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

* Add comment about being able to change setting

* Use <Text> correctly

* Final version of consent screen

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

* Add consent setting to organization settings screen.

* Add support for custom message in OrgSetting.save.

* Implmenet consent saving.

* If consent given, send extra data

* Add HelpTrigger

* Make CodeClimate happy

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

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

* Migrate Custom JS Chart to React

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

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

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

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

* Mjnor update to line/area stacking code

* Fix line/area normalize to percents feature

* Unit tests

* Refine tests; add tests for prepareLayout function

* Tests for prepareData (heatmap) function

* Tests for prepareData (pie) function

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

* Tests for prepareData (scatter, bubble) function

* Tests for prepareData (box) function

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

* Perhaps this would trigger percy

* Decrease size of widget pagination

* Removed unused attr

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

* Migrate Counter to React: Editor

* Cleanup

* Review and fix rows indexing algorithm

* Counter not properly scaled in editor

* Fix wrong label for/input id pair

* Tests

* Tests

* Fix vendor prefixes

* Remove unnecessary useEffect dependencies

* Update tests

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

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

* Added tests

* Fixed snapshot pre-condition

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

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

* Add mixin to __all__

* Switch to flag instead of mixin

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

* Add scheduled status to query job metadata.

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

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

* Add tests

* Update error message

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

* Update README.md

* Update README.md

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

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

* rename pendingParameters to updatedParameters

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

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

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

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

* keeping pep8 standards

* Maintainig pep8 std

* Use latest version of qds-sdk

* Use qds-sdk v1.13.0

* Use qds-sdk v1.12.0

* Use qds-sdk v1.13.0

* Updating SDK with verified version

* hive as default query type

* qds-sdk : Locking most recent release version

* qds-sdk : Locking recent release version

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

* CodeClimate fixes

* Remove TODO

* Fixed configuration properties names for Azure Kusto

* Azure Kusto: get_schema in one query

* azure-kusto-data update to 0.0.32

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

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

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

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

* Add dirtyCount

* Use workaround with setState

* Apply Changes

* Add EditSettingsDialog

* Add Cmd/Ctrl + Enter behavior

* Remove isApplying

* Delete Angular version of parameters

* Update tests

* Remove angular stuff

* Update jest

* Drag placeholder

* Update events

* Use old button styling and move css

* Reviewing code

* Add parameter rearrange test

* Add Parameter Settings title change test

* Update Parameter Settings button styling

* Move parameter url logic back to Parameters

* Disable url update when query is new

* Styling changes (#4019)

* Ran's title width styling

* Update drag test

* Improve sizing for Number inputs

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

* Fix issue with dragged parameter wrapping

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

* Don't reevaluate dirtyParamCount

* Allow multiple values :)

* Fix parameter alignments

* Fix Select width on search

* Update client/app/components/Parameters.less

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

* Humanize param.name

* Make sure angular updates Execute disabled status
2019-10-27 13:42:36 +02:00
198 changed files with 6895 additions and 3350 deletions

View File

@@ -90,21 +90,6 @@ jobs:
- run: - run:
name: Execute Cypress tests name: Execute Cypress tests
command: npm run cypress run-ci command: npm run cypress run-ci
build-tarball:
docker:
- image: circleci/node:8
steps:
- checkout
- run: sudo apt install python-pip
- run: sudo pip install -r requirements_bundles.txt
- run: npm install
- run: .circleci/update_version
- run: npm run bundle
- run: npm run build
- run: rm -rf ./node_modules/
- run: .circleci/pack
- store_artifacts:
path: /tmp/artifacts/
build-docker-image: build-docker-image:
docker: docker:
- image: circleci/node:8 - image: circleci/node:8
@@ -130,17 +115,8 @@ workflows:
- frontend-e2e-tests: - frontend-e2e-tests:
requires: requires:
- frontend-lint - frontend-lint
- build-tarball: - hold:
requires: type: approval
- backend-unit-tests
- frontend-unit-tests
- frontend-e2e-tests
filters:
branches:
only:
- master
- /release\/.*/
- build-docker-image:
requires: requires:
- backend-unit-tests - backend-unit-tests
- frontend-unit-tests - frontend-unit-tests
@@ -151,3 +127,6 @@ workflows:
- master - master
- preview-image - preview-image
- /release\/.*/ - /release\/.*/
- build-docker-image:
requires:
- hold

View File

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

View File

@@ -4,7 +4,7 @@
[![Documentation](https://img.shields.io/badge/docs-redash.io/help-brightgreen.svg)](https://redash.io/help/) [![Documentation](https://img.shields.io/badge/docs-redash.io/help-brightgreen.svg)](https://redash.io/help/)
[![Datree](https://s3.amazonaws.com/catalog.static.datree.io/datree-badge-20px.svg)](https://datree.io/?src=badge) [![Datree](https://s3.amazonaws.com/catalog.static.datree.io/datree-badge-20px.svg)](https://datree.io/?src=badge)
![Build Status](https://circleci.com/gh/getredash/redash.png?circle-token=8a695aa5ec2cbfa89b48c275aea298318016f040) [![Build Status](https://circleci.com/gh/getredash/redash.png?style=shield&circle-token=8a695aa5ec2cbfa89b48c275aea298318016f040)](https://circleci.com/gh/getredash/redash/tree/master)
**_Redash_** is our take on freeing the data within our company in a way that will better fit our culture and usage patterns. **_Redash_** is our take on freeing the data within our company in a way that will better fit our culture and usage patterns.

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -77,12 +77,6 @@
} }
} }
// Fix for Ant dropdowns when they are used in Boootstrap modals
// ANGULAR_REMOVE_ME Remove when all dialogs will be migrated to React (also search and remove usages)
.ant-dropdown-in-bootstrap-modal {
z-index: 1050;
}
// Button overrides // Button overrides
.@{btn-prefix-cls} { .@{btn-prefix-cls} {
transition-duration: 150ms; transition-duration: 150ms;
@@ -156,6 +150,10 @@
border-color: transparent; border-color: transparent;
color: @pagination-color; color: @pagination-color;
line-height: @pagination-item-size - 2px; line-height: @pagination-item-size - 2px;
.@{pagination-prefix-cls}.mini & {
line-height: @pagination-item-size-sm - 2px;
}
} }
&:focus .@{pagination-prefix-cls}-item-link, &:focus .@{pagination-prefix-cls}-item-link,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -154,3 +154,9 @@
Border Radius Border Radius
-----------------------------------------------------------*/ -----------------------------------------------------------*/
.brd-2 { border-radius: 2px; } .brd-2 { border-radius: 2px; }
/* --------------------------------------------------------
Alignment
-----------------------------------------------------------*/
.va-top { vertical-align: top; }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
margin-bottom: 0; margin-bottom: 0;
th.sortable-column { th.sortable-column {
cursor: pointer; cursor: pointer;
} }
&:not(.table-striped) > thead > tr > th { &:not(.table-striped) > thead > tr > th {
@@ -94,6 +94,56 @@
} }
.table-hover > tbody > tr:hover { .table-hover > tbody > tr:hover {
background-color: #f4f4f4; background-color: #f4f4f4;
} }
.table-data {
tbody > tr > td {
padding-top: 5px !important;
}
.btn-favourite, .btn-archive {
font-size: 15px;
}
}
.table-main-title {
font-weight: 500;
line-height: 1.7 !important;
}
.btn-favourite {
color: #d4d4d4;
transition: all .25s ease-in-out;
&:hover, &:focus {
color: @yellow-darker;
cursor: pointer;
}
.fa-star {
color: @yellow-darker;
}
}
.btn-archive {
color: #d4d4d4;
transition: all .25s ease-in-out;
&:hover, &:focus {
color: @gray-light;
}
.fa-archive {
color: @gray-light;
}
}
.table > thead > tr > th {
text-transform: none;
}
.table-data .label-tag {
display: inline-block;
max-width: 135px;
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -32,7 +32,6 @@
@import 'inc/progress-bar'; @import 'inc/progress-bar';
@import 'inc/widgets'; @import 'inc/widgets';
@import 'inc/table'; @import 'inc/table';
@import 'inc/pagination';
@import 'inc/alert'; @import 'inc/alert';
@import 'inc/media'; @import 'inc/media';
@import 'inc/modal'; @import 'inc/modal';
@@ -54,11 +53,9 @@
@import 'inc/schema-browser'; @import 'inc/schema-browser';
@import 'inc/toast'; @import 'inc/toast';
@import 'inc/visualizations/box'; @import 'inc/visualizations/box';
@import 'inc/visualizations/counter-render';
@import 'inc/visualizations/sankey'; @import 'inc/visualizations/sankey';
@import 'inc/visualizations/pivot-table'; @import 'inc/visualizations/pivot-table';
@import 'inc/visualizations/map'; @import 'inc/visualizations/map';
@import 'inc/visualizations/chart';
@import 'inc/visualizations/sunburst'; @import 'inc/visualizations/sunburst';
@import 'inc/visualizations/cohort'; @import 'inc/visualizations/cohort';
@import 'inc/visualizations/misc'; @import 'inc/visualizations/misc';
@@ -71,11 +68,11 @@
@import 'inc/vendor-overrides/ui-select'; @import 'inc/vendor-overrides/ui-select';
/** REDASH STYLING **/ /** REDASH STYLING **/
@import 'redash/redash-newstyle';
@import 'redash/redash-table'; @import 'redash/redash-table';
@import 'redash/query'; @import 'redash/query';
@import 'redash/tags-control'; @import 'redash/tags-control';
@import 'redash/css-logo'; @import 'redash/css-logo';
@import 'redash/loading-indicator';

View File

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

View File

@@ -92,7 +92,7 @@ edit-in-place p.editable:hover {
} }
.filter-container { .filter-container {
margin-bottom: 10px; margin-bottom: 5px;
} }
.ace_editor.ace_autocomplete .ace_completion-highlight { .ace_editor.ace_autocomplete .ace_completion-highlight {
@@ -208,18 +208,18 @@ edit-in-place p.editable:hover {
} }
} }
.visualization-renderer {
.pagination,
.ant-pagination {
margin-top: 10px;
}
}
.embed__vis { .embed__vis {
display: flex; display: flex;
flex-flow: column; flex-flow: column;
} }
.embed-heading {
h3 {
line-height: 1.75;
margin: 0;
}
}
.widget-wrapper { .widget-wrapper {
.body-container { .body-container {
.filters-wrapper { .filters-wrapper {
@@ -343,7 +343,8 @@ a.label-tag {
border-bottom: 1px solid #efefef; border-bottom: 1px solid #efefef;
} }
.pivot-table-renderer > table, grid-renderer > div, visualization-renderer > div { .pivot-table-renderer > table,
visualization-renderer > .visualization-renderer-wrapper {
overflow: visible; overflow: visible;
} }
@@ -676,8 +677,17 @@ nav .rg-bottom {
.filter-container { .filter-container {
padding-right: 0; padding-right: 0;
} }
}
.btn-edit-visualisation { // Responsive fixes
@media (max-width: 767px) {
.query-page-wrapper {
h3 {
font-size: 18px;
}
favorites-control {
margin-top: -3px;
}
} }
} }

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -1,23 +1,12 @@
import React from 'react'; // ANGULAR_REMOVE_ME
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular'; import { react2angular } from 'react2angular';
import ColorPicker from '@/components/ColorPicker';
import './color-box.less'; import './color-box.less';
export function ColorBox({ color }) {
return <span style={{ backgroundColor: color }} />;
}
ColorBox.propTypes = {
color: PropTypes.string,
};
ColorBox.defaultProps = {
color: 'transparent',
};
export default function init(ngModule) { export default function init(ngModule) {
ngModule.component('colorBox', react2angular(ColorBox)); ngModule.component('colorBox', react2angular(ColorPicker.Swatch));
} }
init.init = true; init.init = true;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -153,6 +153,7 @@ function EditParameterSettingsDialog(props) {
<Input <Input
value={isNull(param.title) ? getDefaultTitle(param.name) : param.title} value={isNull(param.title) ? getDefaultTitle(param.name) : param.title}
onChange={e => setParam({ ...param, title: e.target.value })} onChange={e => setParam({ ...param, title: e.target.value })}
data-test="ParameterTitleInput"
/> />
</Form.Item> </Form.Item>
<Form.Item label="Type" {...formItemProps}> <Form.Item label="Type" {...formItemProps}>
@@ -176,7 +177,7 @@ function EditParameterSettingsDialog(props) {
</Select> </Select>
</Form.Item> </Form.Item>
{param.type === 'enum' && ( {param.type === 'enum' && (
<Form.Item label="Values" help="Dropdown list values (newline delimeted)" {...formItemProps}> <Form.Item label="Values" help="Dropdown list values (newline delimited)" {...formItemProps}>
<Input.TextArea <Input.TextArea
rows={3} rows={3}
value={param.enumOptions} value={param.enumOptions}

View File

@@ -32,6 +32,10 @@ export const TYPES = {
'/user-guide/users/authentication-options', '/user-guide/users/authentication-options',
'Guide: Authentication Options', 'Guide: Authentication Options',
], ],
USAGE_DATA_SHARING: [
'/open-source/admin-guide/usage-data',
'Help: Anonymous Usage Data Sharing',
],
DS_ATHENA: [ DS_ATHENA: [
'/data-sources/amazon-athena-setup', '/data-sources/amazon-athena-setup',
'Guide: Help Setting up Amazon Athena', 'Guide: Help Setting up Amazon Athena',

View File

@@ -1,14 +1,13 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import Button from 'antd/lib/button'; import Button from 'antd/lib/button';
import Badge from 'antd/lib/badge'; import Badge from 'antd/lib/badge';
import Tooltip from 'antd/lib/tooltip'; import Tooltip from 'antd/lib/tooltip';
import { KeyboardShortcuts } from '@/services/keyboard-shortcuts'; import { KeyboardShortcuts } from '@/services/keyboard-shortcuts';
function ParameterApplyButton({ paramCount, onClick, isApplying }) { function ParameterApplyButton({ paramCount, onClick }) {
// show spinner when applying (also when count is empty so the fade out is consistent) // show spinner when count is empty so the fade out is consistent
const icon = isApplying || !paramCount ? 'spinner fa-pulse' : 'check'; const icon = !paramCount ? 'spinner fa-pulse' : 'check';
return ( return (
<div className="parameter-apply-button" data-show={!!paramCount} data-test="ParameterApplyButton"> <div className="parameter-apply-button" data-show={!!paramCount} data-test="ParameterApplyButton">
@@ -28,11 +27,6 @@ function ParameterApplyButton({ paramCount, onClick, isApplying }) {
ParameterApplyButton.propTypes = { ParameterApplyButton.propTypes = {
onClick: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired,
paramCount: PropTypes.number.isRequired, paramCount: PropTypes.number.isRequired,
isApplying: PropTypes.bool.isRequired,
}; };
export default function init(ngModule) { export default ParameterApplyButton;
ngModule.component('parameterApplyButton', react2angular(ParameterApplyButton));
}
init.init = true;

View File

@@ -14,7 +14,7 @@ import Input from 'antd/lib/input';
import Radio from 'antd/lib/radio'; import Radio from 'antd/lib/radio';
import Form from 'antd/lib/form'; import Form from 'antd/lib/form';
import Tooltip from 'antd/lib/tooltip'; import Tooltip from 'antd/lib/tooltip';
import { ParameterValueInput } from '@/components/ParameterValueInput'; import ParameterValueInput from '@/components/ParameterValueInput';
import { ParameterMappingType } from '@/services/widget'; import { ParameterMappingType } from '@/services/widget';
import { Parameter } from '@/services/query'; import { Parameter } from '@/services/query';
import { HelpTrigger } from '@/components/HelpTrigger'; import { HelpTrigger } from '@/components/HelpTrigger';

View File

@@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import Select from 'antd/lib/select'; import Select from 'antd/lib/select';
import Input from 'antd/lib/input'; import Input from 'antd/lib/input';
import InputNumber from 'antd/lib/input-number'; import InputNumber from 'antd/lib/input-number';
@@ -19,7 +18,7 @@ const multipleValuesProps = {
maxTagPlaceholder: num => `+${num.length} more`, maxTagPlaceholder: num => `+${num.length} more`,
}; };
export class ParameterValueInput extends React.Component { class ParameterValueInput extends React.Component {
static propTypes = { static propTypes = {
type: PropTypes.string, type: PropTypes.string,
value: PropTypes.any, // eslint-disable-line react/forbid-prop-types value: PropTypes.any, // eslint-disable-line react/forbid-prop-types
@@ -108,7 +107,6 @@ export class ParameterValueInput extends React.Component {
value={value} value={value}
onChange={this.onSelect} onChange={this.onSelect}
dropdownMatchSelectWidth={false} dropdownMatchSelectWidth={false}
dropdownClassName="ant-dropdown-in-bootstrap-modal"
showSearch showSearch
showArrow showArrow
style={{ minWidth: 60 }} style={{ minWidth: 60 }}
@@ -142,7 +140,7 @@ export class ParameterValueInput extends React.Component {
const { className } = this.props; const { className } = this.props;
const { value } = this.state; const { value } = this.state;
const normalize = val => !isNaN(val) && val || 0; const normalize = val => (isNaN(val) ? undefined : val);
return ( return (
<InputNumber <InputNumber
@@ -194,34 +192,4 @@ export class ParameterValueInput extends React.Component {
} }
} }
export default function init(ngModule) { export default ParameterValueInput;
ngModule.component('parameterValueInput', {
template: `
<parameter-value-input-impl
type="$ctrl.param.type"
value="$ctrl.param.normalizedValue"
parameter="$ctrl.param"
enum-options="$ctrl.param.enumOptions"
query-id="$ctrl.param.queryId"
allow-multiple-values="!!$ctrl.param.multiValuesOptions"
on-select="$ctrl.setValue"
></parameter-value-input-impl>
`,
bindings: {
param: '<',
},
controller($scope) {
this.setValue = (value, isDirty) => {
if (isDirty) {
this.param.setPendingValue(value);
} else {
this.param.clearPendingValue();
}
$scope.$apply();
};
},
});
ngModule.component('parameterValueInputImpl', react2angular(ParameterValueInput));
}
init.init = true;

View File

@@ -5,9 +5,15 @@
.parameter-input { .parameter-input {
display: inline-block; display: inline-block;
position: relative; position: relative;
width: 100%;
.@{ant-prefix}-input[type="text"] { .@{ant-prefix}-input,
width: 195px; .@{ant-prefix}-input-number {
min-width: 100% !important;
}
.@{ant-prefix}-select {
width: 100%;
} }
&[data-dirty] { &[data-dirty] {
@@ -18,65 +24,3 @@
} }
} }
} }
.parameter-container {
position: relative;
.parameter-apply-button {
display: none; // default for mobile
// "floating" on desktop
@media (min-width: 768px) {
position: absolute;
bottom: -42px;
left: -15px;
border-radius: 2px;
z-index: 1;
transition: opacity 150ms ease-out;
box-shadow: 0 4px 9px -3px rgba(102, 136, 153, 0.15);
background-color: #ffffff;
padding: 4px;
padding-left: 16px;
opacity: 0;
display: block;
pointer-events: none; // so tooltip doesn't remain after button hides
}
&[data-show="true"] {
opacity: 1;
display: block;
pointer-events: auto;
}
button {
padding: 0 8px 0 6px;
color: #2096f3;
border-color: #50acf6;
// smaller on desktop
@media (min-width: 768px) {
font-size: 12px;
height: 27px;
}
&:hover, &:focus, &:active {
background-color: #eef7fe;
}
i {
margin-right: 3px;
}
}
.ant-badge-count {
min-width: 15px;
height: 15px;
padding: 0 5px;
font-size: 10px;
line-height: 15px;
background: #f77b74;
border-radius: 7px;
box-shadow: 0px 0px 0 1px white, -1px 1px 0 1px #5d6f7d85;
}
}
}

View File

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

View File

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

View File

@@ -81,7 +81,6 @@ export class QueryBasedParameterInput extends React.Component {
value={isArray(value) ? value : toString(value)} value={isArray(value) ? value : toString(value)}
onChange={onSelect} onChange={onSelect}
dropdownMatchSelectWidth={false} dropdownMatchSelectWidth={false}
dropdownClassName="ant-dropdown-in-bootstrap-modal"
optionFilterProp="children" optionFilterProp="children"
showSearch showSearch
showArrow showArrow

View File

@@ -4,6 +4,8 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import EmptyState from '@/components/items-list/components/EmptyState'; import EmptyState from '@/components/items-list/components/EmptyState';
import './CardsList.less';
const { Search } = Input; const { Search } = Input;
export default class CardsList extends React.Component { export default class CardsList extends React.Component {

View File

@@ -0,0 +1,76 @@
@import '../../assets/less/inc/variables';
.visual-card-list {
margin: -5px 0 0 -5px; // compensate for .visual-card spacing
}
.visual-card {
background: #FFFFFF;
border: 1px solid fade(@redash-gray, 15%);
border-radius: 3px;
margin: 5px;
width: 212px;
padding: 15px 5px;
cursor: pointer;
box-shadow: none;
transition: transform 0.12s ease-out;
transition-duration: 0.3s;
transition-property: box-shadow;
display: flex;
align-items: center;
&:hover {
box-shadow: rgba(102, 136, 153, 0.15) 0px 4px 9px -3px;
}
img {
width: 64px !important;
height: 64px !important;
margin-right: 5px;
}
h3 {
font-size: 13px;
color: #323232;
margin: 0 !important;
text-overflow: ellipsis;
overflow: hidden;
}
}
@media (max-width: 1200px) {
.visual-card {
width: 217px;
}
}
@media (max-width: 755px) {
.visual-card {
width: 47%;
}
}
@media (max-width: 515px) {
.visual-card {
width: 47%;
img {
width: 48px;
height: 48px;
}
}
}
@media (max-width: 408px) {
.visual-card {
width: 100%;
padding: 5px;
img {
width: 48px;
height: 48px;
}
}
}

View File

@@ -1,19 +1,10 @@
@import '../assets/less/inc/variables'; // ANGULAR_REMOVE_ME
color-box { color-box {
vertical-align: text-bottom; vertical-align: text-bottom;
display: inline; display: inline-block;
margin-right: 5px;
span { & ~ span {
width: 12px !important;
height: 12px !important;
display: inline-block !important;
margin-right: 5px;
vertical-align: middle;
border: 1px solid rgba(0,0,0,0.1);
}
& ~ span {
vertical-align: bottom; vertical-align: bottom;
color: @input-color; }
}
} }

View File

@@ -5,7 +5,7 @@
</div> </div>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<visualization-renderer visualization="$ctrl.widget.visualization" query-result="$ctrl.widget.getQueryResult()" class="t-body"></visualization-renderer> <visualization-renderer visualization="$ctrl.widget.visualization" query-result="$ctrl.widget.getQueryResult()" class="t-body" context="'widget'"></visualization-renderer>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-default" ng-click="$ctrl.dismiss()">Close</button> <button type="button" class="btn btn-default" ng-click="$ctrl.dismiss()">Close</button>

View File

@@ -44,7 +44,7 @@
</div> </div>
</div> </div>
<div class="m-b-10" ng-if="$ctrl.localParametersDefs().length > 0"> <div class="m-b-10" ng-if="$ctrl.localParametersDefs().length > 0">
<parameters parameters="$ctrl.localParametersDefs()" on-values-change="$ctrl.refresh"></parameters> <parameters parameters="$ctrl.localParametersDefs()" on-values-change="$ctrl.forceRefresh"></parameters>
</div> </div>
</div> </div>
@@ -56,6 +56,7 @@
visualization="$ctrl.widget.visualization" visualization="$ctrl.widget.visualization"
query-result="$ctrl.widget.getQueryResult()" query-result="$ctrl.widget.getQueryResult()"
filters="$ctrl.filters" filters="$ctrl.filters"
context="'widget'"
></visualization-renderer> ></visualization-renderer>
</div> </div>
<div ng-switch-default class="body-row-auto spinner-container"> <div ng-switch-default class="body-row-auto spinner-container">
@@ -64,22 +65,26 @@
</div> </div>
</div> </div>
<div class="body-row clearfix tile__bottom-control"> <div class="body-row tile__bottom-control">
<a class="refresh-button hidden-print btn btn-sm btn-default btn-transparent" ng-click="$ctrl.refresh(1)" ng-if="!$ctrl.public && !!$ctrl.widget.getQueryResult()" data-test="RefreshButton"> <span>
<i class="zmdi zmdi-refresh" ng-class="{ 'zmdi-hc-spin': $ctrl.refreshClickButtonId === 1}"></i> <a class="refresh-button hidden-print btn btn-sm btn-default btn-transparent" ng-click="$ctrl.refresh(1)" ng-if="!$ctrl.public && !!$ctrl.widget.getQueryResult()" data-test="RefreshButton">
<span am-time-ago="$ctrl.widget.getQueryResult().getUpdatedAt()"></span> <i class="zmdi zmdi-refresh" ng-class="{ 'zmdi-hc-spin': $ctrl.refreshClickButtonId === 1}"></i>
</a> <span am-time-ago="$ctrl.widget.getQueryResult().getUpdatedAt()"></span>
<span class="small hidden-print" ng-if="$ctrl.public"> </a>
<i class="zmdi zmdi-time-restore"></i> <span am-time-ago="$ctrl.widget.getQueryResult().getUpdatedAt()"></span> <span class="small hidden-print" ng-if="$ctrl.public">
</span> <i class="zmdi zmdi-time-restore"></i> <span am-time-ago="$ctrl.widget.getQueryResult().getUpdatedAt()"></span>
<span class="visible-print"> </span>
<i class="zmdi zmdi-time-restore"></i> {{$ctrl.widget.getQueryResult().getUpdatedAt() | dateTime}} <span class="visible-print">
<i class="zmdi zmdi-time-restore"></i> {{$ctrl.widget.getQueryResult().getUpdatedAt() | dateTime}}
</span>
</span> </span>
<button class="btn btn-sm btn-default pull-right hidden-print btn-transparent btn__refresh" ng-click="$ctrl.refresh(2)" ng-if="!$ctrl.public"> <span>
<i class="zmdi zmdi-refresh" ng-class="{ 'zmdi-hc-spin': $ctrl.refreshClickButtonId === 2}"></i> <button class="btn btn-sm btn-default hidden-print btn-transparent btn__refresh" ng-click="$ctrl.expandVisualization()"><i class="zmdi zmdi-fullscreen"></i></button>
</button> <button class="btn btn-sm btn-default hidden-print btn-transparent btn__refresh" ng-click="$ctrl.refresh(2)" ng-if="!$ctrl.public">
<button class="btn btn-sm btn-default pull-right hidden-print btn-transparent btn__refresh" ng-click="$ctrl.expandVisualization()"><i class="zmdi zmdi-fullscreen"></i></button> <i class="zmdi zmdi-refresh" ng-class="{ 'zmdi-hc-spin': $ctrl.refreshClickButtonId === 2}"></i>
</button>
</span>
</div> </div>
</div> </div>

View File

@@ -92,6 +92,8 @@ function DashboardWidgetCtrl($scope, $location, $uibModal, $window, $rootScope,
return this.widget.load(refresh, maxAge); return this.widget.load(refresh, maxAge);
}; };
this.forceRefresh = () => this.load(true);
this.refresh = (buttonId) => { this.refresh = (buttonId) => {
this.refreshClickButtonId = buttonId; this.refreshClickButtonId = buttonId;
this.load(true).finally(() => { this.load(true).finally(() => {

View File

@@ -1,3 +1,5 @@
@import '../../assets/less/inc/variables';
.tile .t-header .th-title a.query-link { .tile .t-header .th-title a.query-link {
color: rgba(0, 0, 0, 0.5); color: rgba(0, 0, 0, 0.5);
} }
@@ -26,6 +28,10 @@ visualization-name {
} }
.widget-wrapper { .widget-wrapper {
.parameter-container {
margin: 0 15px;
}
.body-container { .body-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -90,6 +96,181 @@ visualization-name {
} }
} }
.editing-mode {
.widget-menu-regular {
display: none;
}
.widget-menu-remove {
display: block;
}
a.query-link {
pointer-events: none;
cursor: move;
}
.th-title {
cursor: move;
}
.refresh-indicator {
transition-duration: 0s;
rd-timer {
display: none;
}
.refresh-indicator-mini();
}
}
.refresh-indicator {
font-size: 18px;
color: #86a1af;
transition: all 100ms linear;
transition-delay: 150ms; // waits for widget-menu to fade out before moving back over it
transform: translateX(22px);
position: absolute;
right: 29px;
top: 8px;
display: flex;
flex-direction: row-reverse;
.refresh-icon {
position: relative;
&:before {
content: "";
position: absolute;
top: 0px;
right: 0;
width: 24px;
height: 24px;
background-color: #e8ecf0;
border-radius: 50%;
transition: opacity 100ms linear;
transition-delay: 150ms;
}
i {
height: 24px;
width: 24px;
display: flex;
justify-content: center;
align-items: center;
}
}
rd-timer {
font-size: 13px;
display: inline-block;
font-variant-numeric: tabular-nums;
opacity: 0;
transform: translateX(-6px);
transition: all 100ms linear;
transition-delay: 150ms;
color: #bbbbbb;
background-color: rgba(255,255,255,.9);
padding-left: 2px;
padding-right: 1px;
margin-right: -4px;
margin-top: 2px;
}
.widget-visualization[data-refreshing="false"] & {
display: none;
}
}
.refresh-indicator-mini() {
font-size: 13px;
transition-delay: 0s;
color: #bbbbbb;
transform: translateY(-4px);
.refresh-icon:before {
transition-delay: 0s;
opacity: 0;
}
rd-timer {
transition-delay: 0s;
opacity: 1;
transform: translateX(0);
}
}
.tile {
.widget-menu-regular, .btn__refresh {
opacity: 0 !important;
transition: opacity 0.35s ease-in-out;
}
.t-header {
.th-title {
padding-right: 23px; // no overlap on RefreshIndicator
a {
color: fade(@redash-black, 80%);
font-size: 15px;
font-weight: 500;
}
}
.query--description {
font-size: 14px;
line-height: 1.5;
font-style: italic;
p {
margin-bottom: 0;
}
}
}
.t-header.widget {
padding: 15px;
}
&:hover {
.widget-menu-regular, .btn__refresh {
opacity: 1 !important;
transition: opacity 0.35s ease-in-out;
}
.refresh-indicator {
.refresh-indicator-mini();
}
}
.tile__bottom-control {
padding: 10px 15px;
display: flex;
justify-content: space-between;
align-items: center;
.btn-transparent {
&:first-child {
margin-left: -10px;
}
&:last-child {
margin-right: -10px;
}
}
a {
color: fade(@redash-black, 65%);
&:hover {
color: fade(@redash-black, 95%);
}
}
}
}
// react-grid-layout overrides // react-grid-layout overrides
.react-grid-item { .react-grid-item {

View File

@@ -72,6 +72,7 @@ Columns.custom.sortable = sortable;
export default class ItemsTable extends React.Component { export default class ItemsTable extends React.Component {
static propTypes = { static propTypes = {
loading: PropTypes.bool,
// eslint-disable-next-line react/forbid-prop-types // eslint-disable-next-line react/forbid-prop-types
items: PropTypes.arrayOf(PropTypes.object), items: PropTypes.arrayOf(PropTypes.object),
columns: PropTypes.arrayOf(PropTypes.shape({ columns: PropTypes.arrayOf(PropTypes.shape({
@@ -89,6 +90,7 @@ export default class ItemsTable extends React.Component {
}; };
static defaultProps = { static defaultProps = {
loading: false,
items: [], items: [],
columns: [], columns: [],
showHeader: true, showHeader: true,
@@ -150,6 +152,7 @@ export default class ItemsTable extends React.Component {
return ( return (
<Table <Table
className={classNames('table-data', { 'ant-table-headerless': !showHeader })} className={classNames('table-data', { 'ant-table-headerless': !showHeader })}
loading={this.props.loading}
columns={columns} columns={columns}
showHeader={showHeader} showHeader={showHeader}
dataSource={rows} dataSource={rows}

View File

@@ -1,24 +0,0 @@
<div
class="parameter-container form-inline bg-white"
ng-if="parameters | notEmpty"
ui-sortable="{ 'ui-floating': true, 'disabled': !editable }"
ng-model="parameters"
>
<div
class="form-group m-r-10"
ng-repeat="param in parameters"
data-test="ParameterName-{{ param.name }}"
>
<label class="parameter-label">{{ param.title }}</label>
<button
class="btn btn-default btn-xs"
ng-if="editable"
ng-click="showParameterSettings(param, $index)"
data-test="ParameterSettings-{{ param.name }}"
>
<i class="zmdi zmdi-settings"></i>
</button>
<parameter-value-input param="param"></parameter-value-input>
</div>
<parameter-apply-button on-click="onApply" is-applying="isApplying" param-count="dirtyParamCount"></parameter-apply-button>
</div>

View File

@@ -1,98 +0,0 @@
import { extend, filter, forEach, size } from 'lodash';
import template from './parameters.html';
import EditParameterSettingsDialog from './EditParameterSettingsDialog';
function ParametersDirective($location, KeyboardShortcuts) {
return {
restrict: 'E',
transclude: true,
scope: {
parameters: '=',
syncValues: '=?',
editable: '=?',
changed: '&onChange',
onUpdated: '=',
onValuesChange: '=',
applyOnKeyboardShortcut: '<?',
},
template,
link(scope, $element) {
const el = $element.get(0);
const shortcuts = {
'mod+enter': () => scope.onApply(),
'alt+enter': () => scope.onApply(),
};
const onFocus = () => { KeyboardShortcuts.bind(shortcuts); };
const onBlur = () => { KeyboardShortcuts.unbind(shortcuts); };
el.addEventListener('focus', onFocus, true);
el.addEventListener('blur', onBlur, true);
scope.$on('$destroy', () => {
KeyboardShortcuts.unbind(shortcuts);
el.removeEventListener('focus', onFocus);
el.removeEventListener('blur', onBlur);
});
// is this the correct location for this logic?
if (scope.syncValues !== false) {
scope.$watch(
'parameters',
() => {
if (scope.changed) {
scope.changed({});
}
const params = extend({}, $location.search());
scope.parameters.forEach((param) => {
extend(params, param.toUrlParams());
});
Object.keys(params).forEach(key => params[key] == null && delete params[key]);
$location.search(params);
},
true,
);
}
scope.showParameterSettings = (parameter, index) => {
EditParameterSettingsDialog
.showModal({ parameter })
.result.then((updated) => {
scope.parameters[index] = extend(parameter, updated).setValue(updated.value);
scope.onUpdated();
});
};
scope.dirtyParamCount = 0;
scope.$watch(
'parameters',
() => {
scope.dirtyParamCount = size(filter(scope.parameters, 'hasPendingValue'));
},
true,
);
scope.isApplying = false;
scope.applyChanges = () => {
scope.isApplying = true;
forEach(scope.parameters, p => p.applyPendingValue());
scope.isApplying = false;
};
scope.onApply = () => {
if (!scope.dirtyParamCount) {
return false; // so keyboard shortcut doesn't run needlessly
}
scope.$apply(scope.applyChanges);
scope.onValuesChange();
};
},
};
}
export default function init(ngModule) {
ngModule.directive('parameters', ParametersDirective);
}
init.init = true;

View File

@@ -20,44 +20,43 @@
<div class="alert alert-danger" data-test="ErrorMessage">Error: {{$ctrl.error}}</div> <div class="alert alert-danger" data-test="ErrorMessage">Error: {{$ctrl.error}}</div>
</div> </div>
<visualization-renderer visualization="$ctrl.visualization" query-result="$ctrl.queryResult" class="t-body" ng-if="$ctrl.queryResult"> <visualization-renderer visualization="$ctrl.visualization" query-result="$ctrl.queryResult" class="t-body" ng-if="$ctrl.queryResult" context="'widget'">
</visualization-renderer> </visualization-renderer>
</div> </div>
<div class="clearfix tile__bottom-control"> <div class="tile__bottom-control">
<div class="row"> <span>
<div class="col-xs-6"> <a class="small hidden-print" ng-click="$ctrl.refreshQueryResults()">
<a class="small hidden-print" ng-click="$ctrl.refreshQueryResults()"> <i ng-class='{"zmdi-hc-spin": $ctrl.loading}' class="zmdi zmdi-refresh"></i>
<i ng-class='{"zmdi-hc-spin": $ctrl.loading}' class="zmdi zmdi-refresh"></i> <span am-time-ago="$ctrl.queryResult.getUpdatedAt()" ng-if="!$ctrl.loading"></span>
<span am-time-ago="$ctrl.queryResult.getUpdatedAt()" ng-if="!$ctrl.loading"></span> <rd-timer from="$ctrl.refreshStartedAt" ng-if="$ctrl.loading"></rd-timer>
<rd-timer from="$ctrl.refreshStartedAt" ng-if="$ctrl.loading"></rd-timer> </a>
</a> <span class="small visible-print"><i class="zmdi zmdi-time-restore"></i> {{$ctrl.queryResult.getUpdatedAt() | dateTime}} UTC</span>
<span class="small visible-print"><i class="zmdi zmdi-time-restore"></i> {{$ctrl.queryResult.getUpdatedAt() | dateTime}} UTC</span> </span>
</div>
<div class="col-xs-6 text-right hidden-print" ng-if="!$ctrl.hideQueryLink">
<a class="btn btn-default btn-sm" ng-href="{{$ctrl.query.getUrl()}}" target="_blank" tooltip="Open in Redash">
<span class="zmdi zmdi-link"></span>
</a>
<div class="btn-group dropup" uib-dropdown ng-if="!$ctrl.query.hasParameters()"> <span class="hidden-print" ng-if="!$ctrl.hideQueryLink">
<button type="button" class="btn btn-default btn-sm dropdown-toggle" aria-haspopup="true" uib-dropdown-toggle <a class="btn btn-default btn-sm" ng-href="{{$ctrl.query.getUrl()}}" target="_blank" tooltip="Open in Redash">
aria-expanded="false"> <span class="zmdi zmdi-link"></span>
Download Dataset <span class="caret"></span> </a>
</button>
<ul class="dropdown-menu pull-right" uib-dropdown-menu> <div class="btn-group dropup" uib-dropdown ng-if="!$ctrl.query.hasParameters()">
<li> <button type="button" class="btn btn-default btn-sm dropdown-toggle" aria-haspopup="true" uib-dropdown-toggle
<a ng-href="{{$ctrl.queryResult.getLink($ctrl.query.id, 'csv', $ctrl.apiKey)}}" download="{{$ctrl.queryResult.getName($ctrl.query.name, 'csv')}}" target="_self"> aria-expanded="false">
<span class="fa fa-file-o"></span> Download as CSV File Download Dataset <span class="caret"></span>
</a> </button>
</li> <ul class="dropdown-menu pull-right" uib-dropdown-menu>
<li> <li>
<a ng-href="{{$ctrl.queryResult.getLink($ctrl.query.id, 'xlsx', $ctrl.apiKey)}}" download="{{$ctrl.queryResult.getName($ctrl.query.name, 'xlsx')}}" target="_self"> <a ng-href="{{$ctrl.queryResult.getLink($ctrl.query.id, 'csv', $ctrl.apiKey)}}" download="{{$ctrl.queryResult.getName($ctrl.query.name, 'csv')}}" target="_self">
<span class="fa fa-file-excel-o"></span> Download as Excel File <span class="fa fa-file-o"></span> Download as CSV File
</a> </a>
</li> </li>
</ul> <li>
</div> <a ng-href="{{$ctrl.queryResult.getLink($ctrl.query.id, 'xlsx', $ctrl.apiKey)}}" download="{{$ctrl.queryResult.getName($ctrl.query.name, 'xlsx')}}" target="_self">
<span class="fa fa-file-excel-o"></span> Download as Excel File
</a>
</li>
</ul>
</div> </div>
</div> </span>
</div> </div>
</div> </div>

View File

@@ -64,15 +64,6 @@ export function createNumberFormatter(format) {
return value => toString(value); return value => toString(value);
} }
export function createFormatter(column) {
switch (column.displayAs) {
case 'number': return createNumberFormatter(column.numberFormat);
case 'boolean': return createBooleanFormatter(column.booleanValues);
case 'datetime': return createDateTimeFormatter(column.dateTimeFormat);
default: return createTextFormatter(column.allowHTML && column.highlightLinks);
}
}
export function formatSimpleTemplate(str, data) { export function formatSimpleTemplate(str, data) {
if (!isString(str)) { if (!isString(str)) {
return ''; return '';

View File

@@ -137,8 +137,16 @@ function DashboardCtrl(
this.extractGlobalParameters(); this.extractGlobalParameters();
}); });
const collectFilters = (dashboard, forceRefresh) => { const collectFilters = (dashboard, forceRefresh, updatedParameters = []) => {
const queryResultPromises = _.compact(this.dashboard.widgets.map((widget) => { const affectedWidgets = updatedParameters.length > 0 ? this.dashboard.widgets.filter(
widget => Object.values(widget.getParameterMappings()).filter(
({ type }) => type === 'dashboard-level',
).some(
({ mapTo }) => _.includes(updatedParameters.map(p => p.name), mapTo),
),
) : this.dashboard.widgets;
const queryResultPromises = _.compact(affectedWidgets.map((widget) => {
widget.getParametersDefs(); // Force widget to read parameters values from URL widget.getParametersDefs(); // Force widget to read parameters values from URL
return widget.load(forceRefresh); return widget.load(forceRefresh);
})); }));
@@ -202,9 +210,9 @@ function DashboardCtrl(
this.loadDashboard(); this.loadDashboard();
this.refreshDashboard = () => { this.refreshDashboard = (parameters) => {
this.refreshInProgress = true; this.refreshInProgress = true;
collectFilters(this.dashboard, true).finally(() => { collectFilters(this.dashboard, true, parameters).finally(() => {
this.refreshInProgress = false; this.refreshInProgress = false;
}); });
}; };

View File

@@ -1,3 +1,5 @@
@import '../../assets/less/inc/variables';
.dashboard-wrapper { .dashboard-wrapper {
flex-grow: 1; flex-grow: 1;
margin-bottom: 85px; margin-bottom: 85px;
@@ -20,7 +22,8 @@
padding: 0; padding: 0;
} }
.pivot-table-renderer > table, grid-renderer > div, visualization-renderer > div { .pivot-table-renderer > table,
visualization-renderer > .visualization-renderer-wrapper {
overflow: visible; overflow: visible;
} }
@@ -51,13 +54,6 @@
background-size: calc((100vw - 15px) / 6) 5px; background-size: calc((100vw - 15px) / 6) 5px;
background-position: -7px 1px; background-position: -7px 1px;
} }
.widget-menu-regular {
display: none;
}
.widget-menu-remove {
display: block;
}
} }
.dashboard-widget-wrapper:not(.widget-auto-height-enabled) { .dashboard-widget-wrapper:not(.widget-auto-height-enabled) {
@@ -70,7 +66,7 @@
right: 0; right: 0;
bottom: 0; bottom: 0;
> div { > .visualization-renderer-wrapper {
flex-grow: 1; flex-grow: 1;
position: relative; position: relative;
} }
@@ -85,7 +81,7 @@
.map-visualization-container, .map-visualization-container,
.word-cloud-visualization-container, .word-cloud-visualization-container,
.box-plot-deprecated-visualization-container, .box-plot-deprecated-visualization-container,
.plotly-chart-container { .chart-visualization-container {
position: absolute; position: absolute;
left: 0; left: 0;
top: 0; top: 0;
@@ -96,7 +92,7 @@
overflow: hidden; overflow: hidden;
} }
counter { .counter-visualization-content {
position: absolute; position: absolute;
left: 10px; left: 10px;
top: 15px; top: 15px;
@@ -126,6 +122,15 @@
margin: 3px 5px 0 0; margin: 3px 5px 0 0;
} }
.dashboard-header {
position: -webkit-sticky; // required for Safari
position: sticky;
background: #f6f7f9;
z-index: 99;
width: 100%;
top: 0;
}
.dashboard-header, .page-header--query { .dashboard-header, .page-header--query {
.tags-control a { .tags-control a {
opacity: 0; opacity: 0;
@@ -140,6 +145,8 @@
} }
.dashboard__control { .dashboard__control {
margin: 8px 0;
.save-status { .save-status {
vertical-align: middle; vertical-align: middle;
margin-right: 7px; margin-right: 7px;
@@ -234,3 +241,40 @@ dashboard-grid {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.add-widget-container {
background: #fff;
border-radius: @redash-radius;
padding: 15px;
position: fixed;
left: 15px;
bottom: 20px;
width: calc(~'100% - 30px');
z-index: 99;
box-shadow: fade(@redash-gray, 50%) 0px 7px 29px -3px;
display: flex;
justify-content: space-between;
h2 {
margin: 0;
font-size: 14px;
line-height: 2.1;
font-weight: 400;
.zmdi {
margin: 0;
margin-right: 5px;
font-size: 24px;
position: absolute;
bottom: 18px;
}
span {
padding-left: 30px;
}
}
.btn {
align-self: center;
}
}

View File

@@ -1,9 +1,24 @@
<div class="container"> <div class="container">
<div ng-if="$ctrl.messages.includes('using-deprecated-embed-feature')" class="alert alert-warning"> <div
You have enabled <code>ALLOW_PARAMETERS_IN_EMBEDS</code>. This setting is now deprecated and should be turned off. Parameters in embeds are supported by default. <a href="https://discuss.redash.io/t/support-for-parameters-in-embedded-visualizations/3337" target="_blank">Read more</a>. ng-if="$ctrl.messages.includes('using-deprecated-embed-feature')"
class="alert alert-warning"
>
You have enabled <code>ALLOW_PARAMETERS_IN_EMBEDS</code>. This setting is
now deprecated and should be turned off. Parameters in embeds are supported
by default.
<a
href="https://discuss.redash.io/t/support-for-parameters-in-embedded-visualizations/3337"
target="_blank"
>Read more</a
>.
</div> </div>
<div ng-if="$ctrl.messages.includes('email-not-verified')" class="alert alert-warning"> <div
We have sent an email with a confirmation link to your email address. Please follow the link to verify your email address. <a ng-click="$ctrl.verifyEmail()">Resend email</a>. ng-if="$ctrl.messages.includes('email-not-verified')"
class="alert alert-warning"
>
We have sent an email with a confirmation link to your email address. Please
follow the link to verify your email address.
<a ng-click="$ctrl.verifyEmail()">Resend email</a>.
</div> </div>
<empty-state <empty-state
title="'Welcome to Redash 👋'" title="'Welcome to Redash 👋'"
@@ -31,13 +46,19 @@
</p> </p>
<div class="list-group"> <div class="list-group">
<a ng-href="dashboard/{{dashboard.slug}}" class="list-group-item" ng-repeat="dashboard in $ctrl.favoriteDashboards" <a
ng-if="dashboard.is_favorite"> ng-href="dashboard/{{ dashboard.slug }}"
class="list-group-item"
ng-repeat="dashboard in $ctrl.favoriteDashboards"
ng-if="dashboard.is_favorite"
>
<span class="btn-favourite"> <span class="btn-favourite">
<i class="fa fa-star" aria-hidden="true"></i> <i class="fa fa-star" aria-hidden="true"></i>
</span> </span>
{{dashboard.name}} {{ dashboard.name }}
<span class="label label-default" ng-if="dashboard.is_draft">Unpublished</span> <span class="label label-default" ng-if="dashboard.is_draft"
>Unpublished</span
>
</a> </a>
</div> </div>
</div> </div>
@@ -51,17 +72,24 @@
Favorite <a href="queries">Queries</a> will appear here Favorite <a href="queries">Queries</a> will appear here
</p> </p>
<div class="list-group"> <div class="list-group">
<a ng-href="queries/{{query.id}}" class="list-group-item" ng-repeat="query in $ctrl.favoriteQueries" ng-if="query.is_favorite"> <a
ng-href="queries/{{ query.id }}"
class="list-group-item"
ng-repeat="query in $ctrl.favoriteQueries"
ng-if="query.is_favorite"
>
<span class="btn-favourite"> <span class="btn-favourite">
<i class="fa fa-star" aria-hidden="true"></i> <i class="fa fa-star" aria-hidden="true"></i>
</span> </span>
{{query.name}} {{ query.name }}
<span class="label label-default" ng-if="query.is_draft">Unpublished</span> <span class="label label-default" ng-if="query.is_draft"
>Unpublished</span
>
</a> </a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<beacon-consent></beacon-consent>
</div> </div>

View File

@@ -85,9 +85,12 @@
<div class="editor__left__data-source"> <div class="editor__left__data-source">
<ui-select ng-model="query.data_source_id" remove-selected="false" ng-disabled="!isQueryOwner || !sourceMode" <ui-select ng-model="query.data_source_id" remove-selected="false" ng-disabled="!isQueryOwner || !sourceMode"
on-select="updateDataSource()" data-test="SelectDataSource"> on-select="updateDataSource()" data-test="SelectDataSource">
<ui-select-match placeholder="Select Data Source...">{{$select.selected.name}}</ui-select-match> <ui-select-match placeholder="Select Data Source..." class="align-items-center">
<img ng-src="/static/images/db-logos/{{$select.selected.type}}.png" width="20" height="20" style="vertical-align: top">
{{$select.selected.name}}
</ui-select-match>
<ui-select-choices repeat="ds.id as ds in dataSources | filter:$select.search"> <ui-select-choices repeat="ds.id as ds in dataSources | filter:$select.search">
{{ds.name}} <img ng-src="/static/images/db-logos/{{ds.type}}.png" width="20" height="20" class="m-r-5">{{ds.name}}
</ui-select-choices> </ui-select-choices>
</ui-select> </ui-select>
</div> </div>
@@ -191,8 +194,8 @@
<section class="flex-fill p-relative t-body query-visualizations-wrapper"> <section class="flex-fill p-relative t-body query-visualizations-wrapper">
<div class="d-flex flex-column p-b-15 p-absolute static-position__mobile" style="left: 0; top: 0; right: 0; bottom: 0;"> <div class="d-flex flex-column p-b-15 p-absolute static-position__mobile" style="left: 0; top: 0; right: 0; bottom: 0;">
<div class="p-t-15 p-b-5" ng-if="query.hasParameters()"> <div class="p-t-15 p-b-5" ng-if="query.hasParameters()">
<parameters parameters="query.getParametersDefs()" sync-values="!query.isNew()" editable="sourceMode && canEdit" <parameters parameters="query.getParametersDefs()" editable="sourceMode && canEdit" disable-url-update="query.isNew()"
on-updated="onParametersUpdated" on-values-change="executeQuery"></parameters> on-values-change="executeQuery" on-pending-values-change="applyParametersChanges" on-parameters-edit="onParametersUpdated"></parameters>
</div> </div>
<!-- Query Execution Status --> <!-- Query Execution Status -->
@@ -240,7 +243,7 @@
</li> </li>
</ul> </ul>
<div ng-if="selectedVisualization && queryResult" class="query__vis m-t-15 p-b-15 scrollbox" data-test="QueryPageVisualization{{ selectedVisualization.id }}"> <div ng-if="selectedVisualization && queryResult" class="query__vis m-t-15 p-b-15 scrollbox" data-test="QueryPageVisualization{{ selectedVisualization.id }}">
<visualization-renderer visualization="selectedVisualization" query-result="queryResult"></visualization-renderer> <visualization-renderer visualization="selectedVisualization" query-result="queryResult" context="'query'"></visualization-renderer>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -215,6 +215,10 @@ function QueryViewCtrl(
$scope.loadTags = () => getTags('api/queries/tags').then(tags => map(tags, t => t.name)); $scope.loadTags = () => getTags('api/queries/tags').then(tags => map(tags, t => t.name));
$scope.applyParametersChanges = () => {
$scope.$apply();
};
$scope.saveQuery = (customOptions, data) => { $scope.saveQuery = (customOptions, data) => {
let request = data; let request = data;

View File

@@ -152,7 +152,7 @@ class QuerySnippetsList extends React.Component {
There are no query snippets yet. There are no query snippets yet.
{policy.isCreateQuerySnippetEnabled() && ( {policy.isCreateQuerySnippetEnabled() && (
<div className="m-t-5"> <div className="m-t-5">
<a href="/query_snippets/new">Click here</a> to add one. <a className="clickable" onClick={() => this.showSnippetDialog()}>Click here</a> to add one.
</div> </div>
)} )}
</div> </div>

View File

@@ -10,13 +10,14 @@ import Select from 'antd/lib/select';
import Checkbox from 'antd/lib/checkbox'; import Checkbox from 'antd/lib/checkbox';
import Tooltip from 'antd/lib/tooltip'; import Tooltip from 'antd/lib/tooltip';
import LoadingState from '@/components/items-list/components/LoadingState'; import LoadingState from '@/components/items-list/components/LoadingState';
import { HelpTrigger } from '@/components/HelpTrigger';
import { routesToAngularRoutes } from '@/lib/utils'; import { routesToAngularRoutes } from '@/lib/utils';
import { clientConfig } from '@/services/auth'; import { clientConfig } from '@/services/auth';
import settingsMenu from '@/services/settingsMenu'; import settingsMenu from '@/services/settingsMenu';
import recordEvent from '@/services/recordEvent'; import recordEvent from '@/services/recordEvent';
import OrgSettings from '@/services/organizationSettings'; import OrgSettings from '@/services/organizationSettings';
import { HelpTrigger } from '@/components/HelpTrigger';
import DynamicComponent from '@/components/DynamicComponent';
const Option = Select.Option; const Option = Select.Option;
@@ -155,23 +156,6 @@ class OrganizationSettings extends React.Component {
))} ))}
</Select> </Select>
</Form.Item> </Form.Item>
<Form.Item label="Multi-byte Search">
<Checkbox
name="multi_byte_search_enabled"
checked={formValues.multi_byte_search_enabled}
onChange={e => this.handleChange('multi_byte_search_enabled', e.target.checked)}
>
Enable multi-byte (Chinese, Japanese, and Korean) search for query names and descriptions (slower)
</Checkbox>
</Form.Item>
<Form.Item label="Email Reports">
<Checkbox
name="send_email_on_failed_scheduled_queries"
checked={formValues.send_email_on_failed_scheduled_queries}
onChange={e => this.handleChange('send_email_on_failed_scheduled_queries', e.target.checked)}
>Email query owners when scheduled queries fail
</Checkbox>
</Form.Item>
<Form.Item label="Feature Flags"> <Form.Item label="Feature Flags">
<Checkbox <Checkbox
name="feature_show_permissions_control" name="feature_show_permissions_control"
@@ -181,6 +165,34 @@ class OrganizationSettings extends React.Component {
Enable experimental multiple owners support Enable experimental multiple owners support
</Checkbox> </Checkbox>
</Form.Item> </Form.Item>
<Form.Item>
<Checkbox
name="send_email_on_failed_scheduled_queries"
checked={formValues.send_email_on_failed_scheduled_queries}
onChange={e => this.handleChange('send_email_on_failed_scheduled_queries', e.target.checked)}
>Email query owners when scheduled queries fail
</Checkbox>
</Form.Item>
<Form.Item>
<Checkbox
name="multi_byte_search_enabled"
checked={formValues.multi_byte_search_enabled}
onChange={e => this.handleChange('multi_byte_search_enabled', e.target.checked)}
>
Enable multi-byte (Chinese, Japanese, and Korean) search for query names and descriptions (slower)
</Checkbox>
</Form.Item>
<DynamicComponent name="BeaconConsentSetting">
<Form.Item label={<>Anonymous Usage Data Sharing <HelpTrigger type="USAGE_DATA_SHARING" /></>}>
<Checkbox
name="beacon_consent"
checked={formValues.beacon_consent}
onChange={e => this.handleChange('beacon_consent', e.target.checked)}
>
Help Redash improve by automatically sending anonymous usage data
</Checkbox>
</Form.Item>
</DynamicComponent>
</React.Fragment> </React.Fragment>
); );
} }

View File

@@ -3,10 +3,13 @@ import notification from '@/services/notification';
export default { export default {
get: () => $http.get('api/settings/organization').then(response => response.data), get: () => $http.get('api/settings/organization').then(response => response.data),
save: data => $http.post('api/settings/organization', data).then((response) => { save: (data, message = 'Settings changes saved.') => $http
notification.success('Settings changes saved.'); .post('api/settings/organization', data)
return response.data; .then((response) => {
}).catch(() => { notification.success(message);
notification.error('Failed saving changes.'); return response.data;
}), })
.catch(() => {
notification.error('Failed saving changes.');
}),
}; };

View File

@@ -93,6 +93,10 @@ function collectParams(parts) {
return parameters; return parameters;
} }
function isEmptyValue(value) {
return isNull(value) || isUndefined(value) || (value === '') || (isArray(value) && value.length === 0);
}
function isDateParameter(paramType) { function isDateParameter(paramType) {
return includes(['date', 'datetime-local', 'datetime-with-seconds'], paramType); return includes(['date', 'datetime-local', 'datetime-with-seconds'], paramType);
} }
@@ -164,10 +168,6 @@ export class Parameter {
return isNull(this.getValue()); return isNull(this.getValue());
} }
getValue(extra = {}) {
return this.constructor.getValue(this, extra);
}
get hasDynamicValue() { get hasDynamicValue() {
if (isDateParameter(this.type)) { if (isDateParameter(this.type)) {
return isDynamicDate(this.value); return isDynamicDate(this.value);
@@ -188,9 +188,12 @@ export class Parameter {
return false; return false;
} }
getValue(extra = {}) {
return this.constructor.getValue(this, extra);
}
static getValue(param, extra = {}) { static getValue(param, extra = {}) {
const { value, type, useCurrentDateTime, multiValuesOptions } = param; const { value, type, useCurrentDateTime, multiValuesOptions } = param;
const isEmptyValue = isNull(value) || isUndefined(value) || (value === '') || (isArray(value) && value.length === 0);
if (isDateRangeParameter(type) && param.hasDynamicValue) { if (isDateRangeParameter(type) && param.hasDynamicValue) {
const { dynamicValue } = param; const { dynamicValue } = param;
if (dynamicValue) { if (dynamicValue) {
@@ -211,7 +214,7 @@ export class Parameter {
return null; return null;
} }
if (isEmptyValue) { if (isEmptyValue(value)) {
// keep support for existing useCurentDateTime (not available in UI) // keep support for existing useCurentDateTime (not available in UI)
if ( if (
includes(['date', 'datetime-local', 'datetime-with-seconds'], type) && includes(['date', 'datetime-local', 'datetime-with-seconds'], type) &&
@@ -325,7 +328,11 @@ export class Parameter {
} }
get hasPendingValue() { get hasPendingValue() {
return this.pendingValue !== undefined && this.pendingValue !== this.value; // normalize empty values
const pendingValue = isEmptyValue(this.pendingValue) ? null : this.pendingValue;
const value = isEmptyValue(this.value) ? null : this.value;
return this.pendingValue !== undefined && pendingValue !== value;
} }
get normalizedValue() { get normalizedValue() {
@@ -417,7 +424,7 @@ class Parameters {
const fallback = () => map(this.query.options.parameters, i => i.name); const fallback = () => map(this.query.options.parameters, i => i.name);
let parameters = []; let parameters = [];
if (this.query.query) { if (this.query.query !== undefined) {
try { try {
const parts = Mustache.parse(this.query.query); const parts = Mustache.parse(this.query.query);
parameters = uniq(collectParams(parts)); parameters = uniq(collectParams(parts));

View File

@@ -191,6 +191,7 @@ function EditVisualizationDialog({ dialog, visualization, query, queryResult })
options={options} options={options}
visualizationName={name} visualizationName={name}
onOptionsChange={onOptionsChanged} onOptionsChange={onOptionsChanged}
context="query"
/> />
</div> </div>
</Grid.Col> </Grid.Col>

View File

@@ -60,12 +60,13 @@ export function VisualizationRenderer(props) {
return ( return (
<React.Fragment> <React.Fragment>
{showFilters && <Filters filters={filters} onChange={setFilters} />} {showFilters && <Filters filters={filters} onChange={setFilters} />}
<div> <div className="visualization-renderer-wrapper">
<Renderer <Renderer
key={`visualization${visualization.id}`} key={`visualization${visualization.id}`}
options={options} options={options}
data={filteredData} data={filteredData}
visualizationName={visualization.name} visualizationName={visualization.name}
context={props.context}
/> />
</div> </div>
</React.Fragment> </React.Fragment>
@@ -77,6 +78,7 @@ VisualizationRenderer.propTypes = {
queryResult: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types queryResult: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
filters: FiltersType, filters: FiltersType,
showFilters: PropTypes.bool, showFilters: PropTypes.bool,
context: PropTypes.oneOf(['query', 'widget']).isRequired,
}; };
VisualizationRenderer.defaultProps = { VisualizationRenderer.defaultProps = {

View File

@@ -0,0 +1,48 @@
import React, { useState, useEffect, useMemo } from 'react';
import { RendererPropTypes } from '@/visualizations';
import { clientConfig } from '@/services/auth';
import resizeObserver from '@/services/resizeObserver';
import getChartData from '../getChartData';
import { Plotly, prepareCustomChartData, createCustomChartRenderer } from '../plotly';
export default function CustomPlotlyChart({ options, data }) {
if (!clientConfig.allowCustomJSVisualizations) {
return null;
}
const [container, setContainer] = useState(null);
const renderCustomChart = useMemo(
() => createCustomChartRenderer(options.customCode, options.enableConsoleLogs),
[options.customCode, options.enableConsoleLogs],
);
const plotlyData = useMemo(
() => prepareCustomChartData(getChartData(data.rows, options)),
[options, data],
);
useEffect(() => {
if (container) {
const unwatch = resizeObserver(container, () => {
// Clear existing data with blank data for succeeding codeCall adds data to existing plot.
Plotly.purge(container);
renderCustomChart(plotlyData.x, plotlyData.ys, container, Plotly);
});
return unwatch;
}
}, [container, plotlyData]);
// Cleanup when component destroyed
useEffect(() => {
if (container) {
return () => Plotly.purge(container);
}
}, [container]);
return <div className="chart-visualization-container" ref={setContainer} />;
}
CustomPlotlyChart.propTypes = RendererPropTypes;

View File

@@ -0,0 +1,51 @@
import { isArray, isObject } from 'lodash';
import React, { useState, useEffect } from 'react';
import { RendererPropTypes } from '@/visualizations';
import resizeObserver from '@/services/resizeObserver';
import getChartData from '../getChartData';
import { Plotly, prepareData, prepareLayout, updateData, applyLayoutFixes } from '../plotly';
export default function PlotlyChart({ options, data }) {
const [container, setContainer] = useState(null);
useEffect(() => {
if (container) {
const plotlyOptions = { showLink: false, displaylogo: false };
const chartData = getChartData(data.rows, options);
const plotlyData = prepareData(chartData, options);
const plotlyLayout = prepareLayout(container, options, plotlyData);
// It will auto-purge previous graph
Plotly.newPlot(container, plotlyData, plotlyLayout, plotlyOptions).then(() => {
applyLayoutFixes(container, plotlyLayout, (e, u) => Plotly.relayout(e, u));
});
container.on('plotly_restyle', (updates) => {
// This event is triggered if some plotly data/layout has changed.
// We need to catch only changes of traces visibility to update stacking
if (isArray(updates) && isObject(updates[0]) && updates[0].visible) {
updateData(plotlyData, options);
Plotly.relayout(container, plotlyLayout);
}
});
const unwatch = resizeObserver(container, () => {
applyLayoutFixes(container, plotlyLayout, (e, u) => Plotly.relayout(e, u));
});
return unwatch;
}
}, [options, data, container]);
// Cleanup when component destroyed
useEffect(() => {
if (container) {
return () => Plotly.purge(container);
}
}, [container]);
return <div className="chart-visualization-container" ref={setContainer} />;
}
PlotlyChart.propTypes = RendererPropTypes;

View File

@@ -0,0 +1,16 @@
import React from 'react';
import { RendererPropTypes } from '@/visualizations';
import PlotlyChart from './PlotlyChart';
import CustomPlotlyChart from './CustomPlotlyChart';
import './renderer.less';
export default function Renderer({ options, ...props }) {
if (options.globalSeriesType === 'custom') {
return <CustomPlotlyChart options={options} {...props} />;
}
return <PlotlyChart options={options} {...props} />;
}
Renderer.propTypes = RendererPropTypes;

View File

@@ -1,4 +1,4 @@
.plotly-chart-container { .chart-visualization-container {
height: 400px; height: 400px;
overflow: hidden; overflow: hidden;
} }

View File

@@ -168,13 +168,19 @@
<input type="checkbox" ng-model="$ctrl.options.series.percentValues"> Normalize values to percentage <input type="checkbox" ng-model="$ctrl.options.series.percentValues"> Normalize values to percentage
</label> </label>
</div> </div>
<div ng-if="['bubble', 'scatter'].indexOf($ctrl.options.globalSeriesType) === -1" class="checkbox">
<label class="control-label">
<input type="checkbox" ng-model="$ctrl.options.missingValuesAsZero"> Treat missing/null values as 0
</label>
</div>
</div> </div>
</div> </div>
<div ng-if="$ctrl.options.globalSeriesType == 'custom'"> <div ng-if="$ctrl.options.globalSeriesType == 'custom'">
<div class="form-group"> <div class="form-group">
<label class="control-label">Custom code</label> <label class="control-label">Custom code</label>
<textarea ng-model="$ctrl.options.customCode" class="form-control v-resizable" rows="10"> <textarea ng-model="$ctrl.options.customCode" ng-model-options="{ debounce: 300 }" class="form-control v-resizable" rows="10">
</textarea> </textarea>
</div> </div>

View File

@@ -1,6 +0,0 @@
<div ng-if="$ctrl.options.globalSeriesType != 'custom'">
<plotly-chart options="$ctrl.options" series="$ctrl.chartSeries"></plotly-chart>
</div>
<div ng-if="plotlyOptions.globalSeriesType == 'custom'">
<custom-plotly-chart options="$ctrl.options" series="$ctrl.chartSeries"></custom-plotly-chart>
</div>

View File

@@ -0,0 +1,40 @@
{
"input": {
"data": [
{ "a": 42, "b": 10, "g": "first" },
{ "a": 62, "b": 73, "g": "first" },
{ "a": 21, "b": 82, "g": "second" },
{ "a": 85, "b": 50, "g": "first" },
{ "a": 95, "b": 32, "g": "second" }
],
"options": {
"columnMapping": {
"a": "x",
"b": "y",
"g": "series"
},
"seriesOptions": {}
}
},
"output": {
"data": [
{
"name": "first",
"type": "column",
"data": [
{ "x": 42, "y": 10, "$raw": { "a": 42, "b": 10, "g": "first" } },
{ "x": 62, "y": 73, "$raw": { "a": 62, "b": 73, "g": "first" } },
{ "x": 85, "y": 50, "$raw": { "a": 85, "b": 50, "g": "first" } }
]
},
{
"name": "second",
"type": "column",
"data": [
{ "x": 21, "y": 82, "$raw": { "a": 21, "b": 82, "g": "second" } },
{ "x": 95, "y": 32, "$raw": { "a": 95, "b": 32, "g": "second" } }
]
}
]
}
}

View File

@@ -0,0 +1,41 @@
{
"input": {
"data": [
{ "a": 42, "b": 10, "c": 41, "d": 92 },
{ "a": 62, "b": 73 },
{ "a": 21, "b": null, "c": 33 },
{ "a": 85, "b": 50 },
{ "a": 95 }
],
"options": {
"columnMapping": {
"a": "x",
"b": "y",
"c": "y"
},
"seriesOptions": {}
}
},
"output": {
"data": [
{
"name": "b",
"type": "column",
"data": [
{ "x": 42, "y": 10, "$raw": { "a": 42, "b": 10, "c": 41, "d": 92 } },
{ "x": 62, "y": 73, "$raw": { "a": 62, "b": 73 } },
{ "x": 21, "y": null, "$raw": { "a": 21, "b": null, "c": 33 } },
{ "x": 85, "y": 50, "$raw": { "a": 85, "b": 50 } }
]
},
{
"name": "c",
"type": "column",
"data": [
{ "x": 42, "y": 41, "$raw": { "a": 42, "b": 10, "c": 41, "d": 92 } },
{ "x": 21, "y": 33, "$raw": { "a": 21, "b": null, "c": 33 } }
]
}
]
}
}

View File

@@ -0,0 +1,43 @@
{
"input": {
"data": [
{ "a": 42, "b": 10, "g": "first" },
{ "a": 62, "b": 73, "g": "first" },
{ "a": 21, "b": 82, "g": "second" },
{ "a": 85, "b": 50, "g": "first" },
{ "a": 95, "b": 32, "g": "second" }
],
"options": {
"columnMapping": {
"a": "x",
"b": "y",
"g": "series"
},
"seriesOptions": {
"first": { "zIndex": 2 },
"second": { "zIndex": 1 }
}
}
},
"output": {
"data": [
{
"name": "second",
"type": "column",
"data": [
{ "x": 21, "y": 82, "$raw": { "a": 21, "b": 82, "g": "second" } },
{ "x": 95, "y": 32, "$raw": { "a": 95, "b": 32, "g": "second" } }
]
},
{
"name": "first",
"type": "column",
"data": [
{ "x": 42, "y": 10, "$raw": { "a": 42, "b": 10, "g": "first" } },
{ "x": 62, "y": 73, "$raw": { "a": 62, "b": 73, "g": "first" } },
{ "x": 85, "y": 50, "$raw": { "a": 85, "b": 50, "g": "first" } }
]
}
]
}
}

View File

@@ -0,0 +1,32 @@
{
"input": {
"data": [
{ "a": 42, "b": 10, "c": 41, "d": 92 },
{ "a": 62, "b": 73 },
{ "a": 21, "b": null },
{ "a": 85, "b": 50 },
{ "a": 95 }
],
"options": {
"columnMapping": {
"a": "x",
"b": "y"
},
"seriesOptions": {}
}
},
"output": {
"data": [
{
"name": "b",
"type": "column",
"data": [
{ "x": 42, "y": 10, "$raw": { "a": 42, "b": 10, "c": 41, "d": 92 } },
{ "x": 62, "y": 73, "$raw": { "a": 62, "b": 73 } },
{ "x": 21, "y": null, "$raw": { "a": 21, "b": null } },
{ "x": 85, "y": 50, "$raw": { "a": 85, "b": 50 } }
]
}
]
}
}

View File

@@ -26,12 +26,11 @@ export default function getChartData(data, options) {
let sizeValue = null; let sizeValue = null;
let zValue = null; let zValue = null;
forOwn(row, (v, definition) => { forOwn(row, (value, definition) => {
definition = '' + definition; definition = '' + definition;
const definitionParts = definition.split('::') || definition.split('__'); const definitionParts = definition.split('::') || definition.split('__');
const name = definitionParts[0]; const name = definitionParts[0];
const type = mappings ? mappings[definition] : definitionParts[1]; const type = mappings ? mappings[definition] : definitionParts[1];
let value = v;
if (type === 'unused') { if (type === 'unused') {
return; return;
@@ -42,9 +41,6 @@ export default function getChartData(data, options) {
point[type] = value; point[type] = value;
} }
if (type === 'y') { if (type === 'y') {
if (value == null) {
value = 0;
}
yValues[name] = value; yValues[name] = value;
point[type] = value; point[type] = value;
} }

View File

@@ -0,0 +1,32 @@
/* eslint-disable global-require, import/no-unresolved */
import getChartData from './getChartData';
describe('Visualizations', () => {
describe('Chart', () => {
describe('getChartData', () => {
test('Single series', () => {
const { input, output } = require('./fixtures/getChartData/single-series');
const data = getChartData(input.data, input.options);
expect(data).toEqual(output.data);
});
test('Multiple series: multiple Y mappings', () => {
const { input, output } = require('./fixtures/getChartData/multiple-series-multiple-y');
const data = getChartData(input.data, input.options);
expect(data).toEqual(output.data);
});
test('Multiple series: grouped', () => {
const { input, output } = require('./fixtures/getChartData/multiple-series-grouped');
const data = getChartData(input.data, input.options);
expect(data).toEqual(output.data);
});
test('Multiple series: sorted', () => {
const { input, output } = require('./fixtures/getChartData/multiple-series-sorted');
const data = getChartData(input.data, input.options);
expect(data).toEqual(output.data);
});
});
});
});

View File

@@ -6,9 +6,10 @@ import { registerVisualization } from '@/visualizations';
import { clientConfig } from '@/services/auth'; import { clientConfig } from '@/services/auth';
import ColorPalette from '@/visualizations/ColorPalette'; import ColorPalette from '@/visualizations/ColorPalette';
import getChartData from './getChartData'; import getChartData from './getChartData';
import template from './chart.html';
import editorTemplate from './chart-editor.html'; import editorTemplate from './chart-editor.html';
import Renderer from './Renderer';
const DEFAULT_OPTIONS = { const DEFAULT_OPTIONS = {
globalSeriesType: 'column', globalSeriesType: 'column',
sortX: true, sortX: true,
@@ -27,6 +28,8 @@ const DEFAULT_OPTIONS = {
percentFormat: '0[.]00%', percentFormat: '0[.]00%',
// dateTimeFormat: 'DD/MM/YYYY HH:mm', // will be set from clientConfig // dateTimeFormat: 'DD/MM/YYYY HH:mm', // will be set from clientConfig
textFormat: '', // default: combination of {{ @@yPercent }} ({{ @@y }} ± {{ @@yError }}) textFormat: '', // default: combination of {{ @@yPercent }} ({{ @@y }} ± {{ @@yError }})
missingValuesAsZero: true,
}; };
function initEditorForm(options, columns) { function initEditorForm(options, columns) {
@@ -69,26 +72,6 @@ function initEditorForm(options, columns) {
return result; return result;
} }
const ChartRenderer = {
template,
bindings: {
data: '<',
options: '<',
},
controller($scope) {
this.chartSeries = [];
const update = () => {
if (this.data) {
this.chartSeries = getChartData(this.data.rows, this.options);
}
};
$scope.$watch('$ctrl.data', update);
$scope.$watch('$ctrl.options', update, true);
},
};
const ChartEditor = { const ChartEditor = {
template: editorTemplate, template: editorTemplate,
bindings: { bindings: {
@@ -304,7 +287,6 @@ const ChartEditor = {
}; };
export default function init(ngModule) { export default function init(ngModule) {
ngModule.component('chartRenderer', ChartRenderer);
ngModule.component('chartEditor', ChartEditor); ngModule.component('chartEditor', ChartEditor);
ngModule.run(($injector) => { ngModule.run(($injector) => {
@@ -312,11 +294,21 @@ export default function init(ngModule) {
type: 'CHART', type: 'CHART',
name: 'Chart', name: 'Chart',
isDefault: true, isDefault: true,
getOptions: options => merge({}, DEFAULT_OPTIONS, { getOptions: (options) => {
showDataLabels: options.globalSeriesType === 'pie', const result = merge({}, DEFAULT_OPTIONS, {
dateTimeFormat: clientConfig.dateTimeFormat, showDataLabels: options.globalSeriesType === 'pie',
}, options), dateTimeFormat: clientConfig.dateTimeFormat,
Renderer: angular2react('chartRenderer', ChartRenderer, $injector), }, options);
// Backward compatibility
if (['normal', 'percent'].indexOf(result.series.stacking) >= 0) {
result.series.percentValues = result.series.stacking === 'percent';
result.series.stacking = 'stack';
}
return result;
},
Renderer,
Editor: angular2react('chartEditor', ChartEditor, $injector), Editor: angular2react('chartEditor', ChartEditor, $injector),
defaultColumns: 3, defaultColumns: 3,

View File

@@ -0,0 +1,100 @@
import { find, pick, reduce } from 'lodash';
function fixLegendContainer(plotlyElement) {
const legend = plotlyElement.querySelector('.legend');
if (legend) {
let node = legend.parentNode;
while (node) {
if (node.tagName.toLowerCase() === 'svg') {
node.style.overflow = 'visible';
break;
}
node = node.parentNode;
}
}
}
export default function applyLayoutFixes(plotlyElement, layout, updatePlot) {
// update layout size to plot container
layout.width = Math.floor(plotlyElement.offsetWidth);
layout.height = Math.floor(plotlyElement.offsetHeight);
const transformName = find([
'transform',
'WebkitTransform',
'MozTransform',
'MsTransform',
'OTransform',
], prop => prop in plotlyElement.style);
if (layout.width <= 600) {
// change legend orientation to horizontal; plotly has a bug with this
// legend alignment - it does not preserve enough space under the plot;
// so we'll hack this: update plot (it will re-render legend), compute
// legend height, reduce plot size by legend height (but not less than
// half of plot container's height - legend will have max height equal to
// plot height), re-render plot again and offset legend to the space under
// the plot.
layout.legend = {
orientation: 'h',
// locate legend inside of plot area - otherwise plotly will preserve
// some amount of space under the plot; also this will limit legend height
// to plot's height
y: 0,
x: 0,
xanchor: 'left',
yanchor: 'bottom',
};
// set `overflow: visible` to svg containing legend because later we will
// position legend outside of it
fixLegendContainer(plotlyElement);
updatePlot(plotlyElement, pick(layout, ['width', 'height', 'legend'])).then(() => {
const legend = plotlyElement.querySelector('.legend'); // eslint-disable-line no-shadow
if (legend) {
// compute real height of legend - items may be split into few columnns,
// also scrollbar may be shown
const bounds = reduce(legend.querySelectorAll('.traces'), (result, node) => {
const b = node.getBoundingClientRect();
result = result || b;
return {
top: Math.min(result.top, b.top),
bottom: Math.max(result.bottom, b.bottom),
};
}, null);
// here we have two values:
// 1. height of plot container excluding height of legend items;
// it may be any value between 0 and plot container's height;
// 2. half of plot containers height. Legend cannot be larger than
// plot; if legend is too large, plotly will reduce it's height and
// show a scrollbar; in this case, height of plot === height of legend,
// so we can split container's height half by half between them.
layout.height = Math.floor(Math.max(
layout.height / 2,
layout.height - (bounds.bottom - bounds.top),
));
// offset the legend
legend.style[transformName] = 'translate(0, ' + layout.height + 'px)';
updatePlot(plotlyElement, pick(layout, ['height']));
}
});
} else {
layout.legend = {
orientation: 'v',
// vertical legend will be rendered properly, so just place it to the right
// side of plot
y: 1,
x: 1,
xanchor: 'left',
yanchor: 'top',
};
const legend = plotlyElement.querySelector('.legend');
if (legend) {
legend.style[transformName] = null;
}
updatePlot(plotlyElement, pick(layout, ['width', 'height', 'legend']));
}
}

View File

@@ -0,0 +1,40 @@
import { each } from 'lodash';
import { normalizeValue } from './utils';
export function prepareCustomChartData(series) {
const x = [];
const ys = {};
each(series, ({ name, data }) => {
ys[name] = [];
each(data, (point) => {
x.push(normalizeValue(point.x));
ys[name].push(normalizeValue(point.y));
});
});
return { x, ys };
}
export function createCustomChartRenderer(code, logErrorsToConsole = false) {
// Create a function from custom code; catch syntax errors
let render = () => {};
try {
render = new Function('x, ys, element, Plotly', code); // eslint-disable-line no-new-func
} catch (err) {
if (logErrorsToConsole) {
console.log(`Error while executing custom graph: ${err}`); // eslint-disable-line no-console
}
}
// Return function that will invoke custom code; catch runtime errors
return (x, ys, element, Plotly) => {
try {
render(x, ys, element, Plotly);
} catch (err) {
if (logErrorsToConsole) {
console.log(`Error while executing custom graph: ${err}`); // eslint-disable-line no-console
}
}
};
}

View File

@@ -0,0 +1,56 @@
{
"input": {
"options": {
"globalSeriesType": "column",
"numberFormat": "0,0[.]00000",
"percentFormat": "0[.]00%",
"textFormat": "",
"showDataLabels": true,
"direction": { "type": "counterclockwise" },
"xAxis": { "type": "-", "labels": { "enabled": true } },
"yAxis": [
{ "type": "linear" },
{ "type": "linear", "opposite": true }
],
"series": { "stacking": null, "error_y": { "type": "data", "visible": true } },
"seriesOptions": {
"a": { "type": "column", "color": "red" }
},
"columnMapping": {
"x": "x",
"y1": "y"
},
"missingValuesAsZero": true
},
"data": [
{
"name": "a",
"data": [
{ "x": "x1", "y": 10, "yError": 0 },
{ "x": "x2", "y": 20, "yError": 0 },
{ "x": "x3", "y": 30, "yError": 0 },
{ "x": "x4", "y": 40, "yError": 0 }
]
}
]
},
"output": {
"series": [
{
"visible": true,
"type": "bar",
"name": "a",
"x": ["x1", "x2", "x3", "x4"],
"y": [10, 20, 30, 40],
"error_y": { "array": [0, 0, 0, 0], "color": "red" },
"hoverinfo": "text+x+name",
"hover": [],
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
"textposition": "inside",
"marker": { "color": "red" },
"insidetextfont": { "color": "#333333" },
"yaxis": "y"
}
]
}
}

View File

@@ -0,0 +1,81 @@
{
"input": {
"options": {
"globalSeriesType": "column",
"numberFormat": "0,0[.]00000",
"percentFormat": "0[.]00%",
"textFormat": "",
"showDataLabels": true,
"direction": { "type": "counterclockwise" },
"xAxis": { "type": "-", "labels": { "enabled": true } },
"yAxis": [
{ "type": "linear" },
{ "type": "linear", "opposite": true }
],
"series": { "stacking": "stack", "error_y": { "type": "data", "visible": true }, "percentValues": true },
"seriesOptions": {
"a": { "type": "column", "color": "red" },
"b": { "type": "column", "color": "blue" }
},
"columnMapping": {
"x": "x",
"y1": "y"
},
"missingValuesAsZero": true
},
"data": [
{
"name": "a",
"data": [
{ "x": "x1", "y": 10, "yError": 0 },
{ "x": "x2", "y": 20, "yError": 0 },
{ "x": "x3", "y": 30, "yError": 0 },
{ "x": "x4", "y": 40, "yError": 0 }
]
},
{
"name": "b",
"data": [
{ "x": "x1", "y": 40, "yError": 0 },
{ "x": "x2", "y": 30, "yError": 0 },
{ "x": "x3", "y": 20, "yError": 0 },
{ "x": "x4", "y": 10, "yError": 0 }
]
}
]
},
"output": {
"series": [
{
"visible": true,
"type": "bar",
"name": "a",
"x": ["x1", "x2", "x3", "x4"],
"y": [20, 40, 60, 80],
"error_y": { "array": [0, 0, 0, 0], "color": "red" },
"hoverinfo": "text+x+name",
"hover": [],
"text": ["20% (10 ± 0)", "40% (20 ± 0)", "60% (30 ± 0)", "80% (40 ± 0)"],
"textposition": "inside",
"marker": { "color": "red" },
"insidetextfont": { "color": "#333333" },
"yaxis": "y"
},
{
"visible": true,
"type": "bar",
"name": "b",
"x": ["x1", "x2", "x3", "x4"],
"y": [80, 60, 40, 20],
"error_y": { "array": [0, 0, 0, 0], "color": "blue" },
"hoverinfo": "text+x+name",
"hover": [],
"text": ["80% (40 ± 0)", "60% (30 ± 0)", "40% (20 ± 0)", "20% (10 ± 0)"],
"textposition": "inside",
"marker": { "color": "blue" },
"insidetextfont": { "color": "#333333" },
"yaxis": "y"
}
]
}
}

View File

@@ -0,0 +1,81 @@
{
"input": {
"options": {
"globalSeriesType": "column",
"numberFormat": "0,0[.]00000",
"percentFormat": "0[.]00%",
"textFormat": "",
"showDataLabels": true,
"direction": { "type": "counterclockwise" },
"xAxis": { "type": "-", "labels": { "enabled": true } },
"yAxis": [
{ "type": "linear" },
{ "type": "linear", "opposite": true }
],
"series": { "stacking": "stack", "error_y": { "type": "data", "visible": true } },
"seriesOptions": {
"a": { "type": "column", "color": "red" },
"b": { "type": "column", "color": "blue" }
},
"columnMapping": {
"x": "x",
"y1": "y"
},
"missingValuesAsZero": true
},
"data": [
{
"name": "a",
"data": [
{ "x": "x1", "y": 10, "yError": 0 },
{ "x": "x2", "y": 20, "yError": 0 },
{ "x": "x3", "y": 30, "yError": 0 },
{ "x": "x4", "y": 40, "yError": 0 }
]
},
{
"name": "b",
"data": [
{ "x": "x1", "y": 1, "yError": 0 },
{ "x": "x2", "y": 2, "yError": 0 },
{ "x": "x3", "y": 3, "yError": 0 },
{ "x": "x4", "y": 4, "yError": 0 }
]
}
]
},
"output": {
"series": [
{
"visible": true,
"type": "bar",
"name": "a",
"x": ["x1", "x2", "x3", "x4"],
"y": [10, 20, 30, 40],
"error_y": { "array": [0, 0, 0, 0], "color": "red" },
"hoverinfo": "text+x+name",
"hover": [],
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
"textposition": "inside",
"marker": { "color": "red" },
"insidetextfont": { "color": "#333333" },
"yaxis": "y"
},
{
"visible": true,
"type": "bar",
"name": "b",
"x": ["x1", "x2", "x3", "x4"],
"y": [1, 2, 3, 4],
"error_y": { "array": [0, 0, 0, 0], "color": "blue" },
"hoverinfo": "text+x+name",
"hover": [],
"text": ["1 ± 0", "2 ± 0", "3 ± 0", "4 ± 0"],
"textposition": "inside",
"marker": { "color": "blue" },
"insidetextfont": { "color": "#333333" },
"yaxis": "y"
}
]
}
}

View File

@@ -0,0 +1,57 @@
{
"input": {
"options": {
"globalSeriesType": "box",
"numberFormat": "0,0[.]00000",
"percentFormat": "0[.]00%",
"textFormat": "",
"showDataLabels": true,
"direction": { "type": "counterclockwise" },
"xAxis": { "type": "-", "labels": { "enabled": true } },
"yAxis": [
{ "type": "linear" },
{ "type": "linear", "opposite": true }
],
"series": { "stacking": null, "error_y": { "type": "data", "visible": true } },
"seriesOptions": {
"a": { "type": "box", "color": "red" }
},
"columnMapping": {
"x": "x",
"y1": "y"
},
"missingValuesAsZero": true
},
"data": [
{
"name": "a",
"data": [
{ "x": "x1", "y": 10, "yError": 0 },
{ "x": "x2", "y": 20, "yError": 0 },
{ "x": "x3", "y": 30, "yError": 0 },
{ "x": "x4", "y": 40, "yError": 0 }
]
}
]
},
"output": {
"series": [
{
"visible": true,
"name": "a",
"type": "box",
"mode": "markers",
"boxpoints": "outliers",
"hoverinfo": false,
"marker": { "color": "red", "size": 3 },
"x": ["x1", "x2", "x3", "x4"],
"y": [10, 20, 30, 40],
"error_y": { "array": [0, 0, 0, 0], "color": "red" },
"hover": [],
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
"insidetextfont": { "color": "#333333" },
"yaxis": "y"
}
]
}
}

View File

@@ -0,0 +1,60 @@
{
"input": {
"options": {
"globalSeriesType": "box",
"numberFormat": "0,0[.]00000",
"percentFormat": "0[.]00%",
"textFormat": "",
"showDataLabels": true,
"direction": { "type": "counterclockwise" },
"xAxis": { "type": "-", "labels": { "enabled": true } },
"yAxis": [
{ "type": "linear" },
{ "type": "linear", "opposite": true }
],
"series": { "stacking": null, "error_y": { "type": "data", "visible": true } },
"seriesOptions": {
"a": { "type": "box", "color": "red" }
},
"columnMapping": {
"x": "x",
"y1": "y"
},
"missingValuesAsZero": true,
"showpoints": true
},
"data": [
{
"name": "a",
"data": [
{ "x": "x1", "y": 10, "yError": 0 },
{ "x": "x2", "y": 20, "yError": 0 },
{ "x": "x3", "y": 30, "yError": 0 },
{ "x": "x4", "y": 40, "yError": 0 }
]
}
]
},
"output": {
"series": [
{
"visible": true,
"name": "a",
"type": "box",
"mode": "markers",
"boxpoints": "all",
"jitter": 0.3,
"pointpos": -1.8,
"hoverinfo": false,
"marker": { "color": "red", "size": 3 },
"x": ["x1", "x2", "x3", "x4"],
"y": [10, 20, 30, 40],
"error_y": { "array": [0, 0, 0, 0], "color": "red" },
"hover": [],
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
"insidetextfont": { "color": "#333333" },
"yaxis": "y"
}
]
}
}

View File

@@ -0,0 +1,55 @@
{
"input": {
"options": {
"globalSeriesType": "bubble",
"numberFormat": "0,0[.]00000",
"percentFormat": "0[.]00%",
"textFormat": "",
"showDataLabels": true,
"direction": { "type": "counterclockwise" },
"xAxis": { "type": "-", "labels": { "enabled": true } },
"yAxis": [
{ "type": "linear" },
{ "type": "linear", "opposite": true }
],
"series": { "stacking": null, "error_y": { "type": "data", "visible": true } },
"seriesOptions": {
"a": { "type": "bubble", "color": "red" }
},
"columnMapping": {
"x": "x",
"y1": "y"
},
"missingValuesAsZero": true
},
"data": [
{
"name": "a",
"data": [
{ "x": "x1", "y": 10, "yError": 0, "size": 51 },
{ "x": "x2", "y": 20, "yError": 0, "size": 52 },
{ "x": "x3", "y": 30, "yError": 0, "size": 53 },
{ "x": "x4", "y": 40, "yError": 0, "size": 54 }
]
}
]
},
"output": {
"series": [
{
"visible": true,
"name": "a",
"mode": "markers",
"marker": { "color": "red", "size": [51, 52, 53, 54] },
"x": ["x1", "x2", "x3", "x4"],
"y": [10, 20, 30, 40],
"error_y": { "array": [0, 0, 0, 0], "color": "red" },
"hoverinfo": "text+x+name",
"hover": [],
"text": ["10 ± 0: 51", "20 ± 0: 52", "30 ± 0: 53", "40 ± 0: 54"],
"insidetextfont": { "color": "#333333" },
"yaxis": "y"
}
]
}
}

View File

@@ -0,0 +1,33 @@
{
"input": {
"options": {
"globalSeriesType": "heatmap",
"colorScheme": "Bluered",
"seriesOptions": {},
"showDataLabels": false
},
"data": [
{
"name": "a",
"data": [
{ "x": 12, "y": 21, "zVal": 3 },
{ "x": 11, "y": 22, "zVal": 2 },
{ "x": 11, "y": 21, "zVal": 1 },
{ "x": 12, "y": 22, "zVal": 4 }
]
}
]
},
"output": {
"series": [
{
"x": [12, 11],
"y": [21, 22],
"z": [[3, 1], [4, 2]],
"type": "heatmap",
"name": "",
"colorscale": "Bluered"
}
]
}
}

View File

@@ -0,0 +1,35 @@
{
"input": {
"options": {
"globalSeriesType": "heatmap",
"colorScheme": "Bluered",
"seriesOptions": {},
"showDataLabels": false,
"reverseX": true,
"reverseY": true
},
"data": [
{
"name": "a",
"data": [
{ "x": 12, "y": 21, "zVal": 3 },
{ "x": 11, "y": 22, "zVal": 2 },
{ "x": 11, "y": 21, "zVal": 1 },
{ "x": 12, "y": 22, "zVal": 4 }
]
}
]
},
"output": {
"series": [
{
"x": [11, 12],
"y": [22, 21],
"z": [[2, 4], [1, 3]],
"type": "heatmap",
"name": "",
"colorscale": "Bluered"
}
]
}
}

View File

@@ -0,0 +1,37 @@
{
"input": {
"options": {
"globalSeriesType": "heatmap",
"colorScheme": "Bluered",
"seriesOptions": {},
"showDataLabels": false,
"sortX": true,
"sortY": true,
"reverseX": true,
"reverseY": true
},
"data": [
{
"name": "a",
"data": [
{ "x": 12, "y": 21, "zVal": 3 },
{ "x": 11, "y": 22, "zVal": 2 },
{ "x": 11, "y": 21, "zVal": 1 },
{ "x": 12, "y": 22, "zVal": 4 }
]
}
]
},
"output": {
"series": [
{
"x": [12, 11],
"y": [22, 21],
"z": [[4, 2], [3, 1]],
"type": "heatmap",
"name": "",
"colorscale": "Bluered"
}
]
}
}

View File

@@ -0,0 +1,35 @@
{
"input": {
"options": {
"globalSeriesType": "heatmap",
"colorScheme": "Bluered",
"seriesOptions": {},
"showDataLabels": false,
"sortX": true,
"sortY": true
},
"data": [
{
"name": "a",
"data": [
{ "x": 12, "y": 21, "zVal": 3 },
{ "x": 11, "y": 22, "zVal": 2 },
{ "x": 11, "y": 21, "zVal": 1 },
{ "x": 12, "y": 22, "zVal": 4 }
]
}
]
},
"output": {
"series": [
{
"x": [11, 12],
"y": [21, 22],
"z": [[1, 3], [2, 4]],
"type": "heatmap",
"name": "",
"colorscale": "Bluered"
}
]
}
}

View File

@@ -0,0 +1,44 @@
{
"input": {
"options": {
"globalSeriesType": "heatmap",
"colorScheme": "Bluered",
"seriesOptions": {},
"showDataLabels": true
},
"data": [
{
"name": "a",
"data": [
{ "x": 12, "y": 21, "zVal": 3 },
{ "x": 11, "y": 22, "zVal": 2 },
{ "x": 11, "y": 21, "zVal": 1 },
{ "x": 12, "y": 22, "zVal": 4 }
]
}
]
},
"output": {
"series": [
{
"x": [12, 11],
"y": [21, 22],
"z": [[3, 1], [4, 2]],
"type": "heatmap",
"name": "",
"colorscale": "Bluered"
},
{
"x": [12, 11, 12, 11],
"y": [21, 21, 22, 22],
"mode": "text",
"hoverinfo": "skip",
"showlegend": false,
"text": ["3", "1", "4", "2"],
"textfont": {
"color": ["black", "black", "black", "black"]
}
}
]
}
}

View File

@@ -0,0 +1,55 @@
{
"input": {
"options": {
"globalSeriesType": "line",
"numberFormat": "0,0[.]00000",
"percentFormat": "0[.]00%",
"textFormat": "",
"showDataLabels": true,
"direction": { "type": "counterclockwise" },
"xAxis": { "type": "-", "labels": { "enabled": true } },
"yAxis": [
{ "type": "linear" },
{ "type": "linear", "opposite": true }
],
"series": { "stacking": null, "error_y": { "type": "data", "visible": true } },
"seriesOptions": {
"a": { "type": "line", "color": "red" }
},
"columnMapping": {
"x": "x",
"y1": "y"
},
"missingValuesAsZero": true
},
"data": [
{
"name": "a",
"data": [
{ "x": "x1", "y": 10, "yError": 0 },
{ "x": "x2", "y": 20, "yError": 0 },
{ "x": "x3", "y": 30, "yError": 0 },
{ "x": "x4", "y": 40, "yError": 0 }
]
}
]
},
"output": {
"series": [
{
"visible": true,
"name": "a",
"mode": "lines+text",
"x": ["x1", "x2", "x3", "x4"],
"y": [10, 20, 30, 40],
"error_y": { "array": [0, 0, 0, 0], "color": "red" },
"hoverinfo": "text+x+name",
"hover": [],
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
"marker": { "color": "red" },
"insidetextfont": { "color": "#333333" },
"yaxis": "y"
}
]
}
}

View File

@@ -0,0 +1,77 @@
{
"input": {
"options": {
"globalSeriesType": "line",
"numberFormat": "0,0[.]00000",
"percentFormat": "0[.]00%",
"textFormat": "",
"showDataLabels": true,
"direction": { "type": "counterclockwise" },
"xAxis": { "type": "-", "labels": { "enabled": true } },
"yAxis": [
{ "type": "linear" },
{ "type": "linear", "opposite": true }
],
"series": { "stacking": "stack", "error_y": { "type": "data", "visible": true } },
"seriesOptions": {
"a": { "type": "line", "color": "red" },
"b": { "type": "line", "color": "blue" }
},
"columnMapping": {
"x": "x",
"y1": "y"
},
"missingValuesAsZero": false
},
"data": [
{
"name": "a",
"data": [
{ "x": "x1", "y": 10, "yError": 0 },
{ "x": "x2", "y": 20, "yError": 0 },
{ "x": "x3", "y": 30, "yError": 0 },
{ "x": "x4", "y": 40, "yError": 0 }
]
},
{
"name": "b",
"data": [
{ "x": "x2", "y": 2, "yError": 0 },
{ "x": "x4", "y": 4, "yError": 0 }
]
}
]
},
"output": {
"series": [
{
"visible": true,
"name": "a",
"mode": "lines+text",
"x": ["x1", "x2", "x3", "x4"],
"y": [10, 20, 30, 40],
"error_y": { "array": [0, 0, 0, 0], "color": "red" },
"hoverinfo": "text+x+name",
"hover": [],
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
"marker": { "color": "red" },
"insidetextfont": { "color": "#333333" },
"yaxis": "y"
},
{
"visible": true,
"name": "b",
"mode": "lines+text",
"x": ["x1", "x2", "x3", "x4"],
"y": [null, 22, null, 44],
"error_y": { "array": [null, 0, null, 0], "color": "blue" },
"hoverinfo": "text+x+name",
"hover": [],
"text": ["", "2 ± 0", "", "4 ± 0"],
"marker": { "color": "blue" },
"insidetextfont": { "color": "#333333" },
"yaxis": "y"
}
]
}
}

View File

@@ -0,0 +1,77 @@
{
"input": {
"options": {
"globalSeriesType": "line",
"numberFormat": "0,0[.]00000",
"percentFormat": "0[.]00%",
"textFormat": "",
"showDataLabels": true,
"direction": { "type": "counterclockwise" },
"xAxis": { "type": "-", "labels": { "enabled": true } },
"yAxis": [
{ "type": "linear" },
{ "type": "linear", "opposite": true }
],
"series": { "stacking": "stack", "error_y": { "type": "data", "visible": true } },
"seriesOptions": {
"a": { "type": "line", "color": "red" },
"b": { "type": "line", "color": "blue" }
},
"columnMapping": {
"x": "x",
"y1": "y"
},
"missingValuesAsZero": true
},
"data": [
{
"name": "a",
"data": [
{ "x": "x1", "y": 10, "yError": 0 },
{ "x": "x2", "y": 20, "yError": 0 },
{ "x": "x3", "y": 30, "yError": 0 },
{ "x": "x4", "y": 40, "yError": 0 }
]
},
{
"name": "b",
"data": [
{ "x": "x2", "y": 2, "yError": 0 },
{ "x": "x4", "y": 4, "yError": 0 }
]
}
]
},
"output": {
"series": [
{
"visible": true,
"name": "a",
"mode": "lines+text",
"x": ["x1", "x2", "x3", "x4"],
"y": [10, 20, 30, 40],
"error_y": { "array": [0, 0, 0, 0], "color": "red" },
"hoverinfo": "text+x+name",
"hover": [],
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
"marker": { "color": "red" },
"insidetextfont": { "color": "#333333" },
"yaxis": "y"
},
{
"visible": true,
"name": "b",
"mode": "lines+text",
"x": ["x1", "x2", "x3", "x4"],
"y": [10, 22, 30, 44],
"error_y": { "array": [null, 0, null, 0], "color": "blue" },
"hoverinfo": "text+x+name",
"hover": [],
"text": ["0", "2 ± 0", "0", "4 ± 0"],
"marker": { "color": "blue" },
"insidetextfont": { "color": "#333333" },
"yaxis": "y"
}
]
}
}

View File

@@ -0,0 +1,79 @@
{
"input": {
"options": {
"globalSeriesType": "line",
"numberFormat": "0,0[.]00000",
"percentFormat": "0[.]00%",
"textFormat": "",
"showDataLabels": true,
"direction": { "type": "counterclockwise" },
"xAxis": { "type": "-", "labels": { "enabled": true } },
"yAxis": [
{ "type": "linear" },
{ "type": "linear", "opposite": true }
],
"series": { "stacking": "stack", "error_y": { "type": "data", "visible": true }, "percentValues": true },
"seriesOptions": {
"a": { "type": "line", "color": "red" },
"b": { "type": "line", "color": "blue" }
},
"columnMapping": {
"x": "x",
"y1": "y"
},
"missingValuesAsZero": true
},
"data": [
{
"name": "a",
"data": [
{ "x": "x1", "y": 10, "yError": 0 },
{ "x": "x2", "y": 20, "yError": 0 },
{ "x": "x3", "y": 30, "yError": 0 },
{ "x": "x4", "y": 40, "yError": 0 }
]
},
{
"name": "b",
"data": [
{ "x": "x1", "y": 40, "yError": 0 },
{ "x": "x2", "y": 30, "yError": 0 },
{ "x": "x3", "y": 20, "yError": 0 },
{ "x": "x4", "y": 10, "yError": 0 }
]
}
]
},
"output": {
"series": [
{
"visible": true,
"name": "a",
"mode": "lines+text",
"x": ["x1", "x2", "x3", "x4"],
"y": [20, 40, 60, 80],
"error_y": { "array": [0, 0, 0, 0], "color": "red" },
"hoverinfo": "text+x+name",
"hover": [],
"text": ["20% (10 ± 0)", "40% (20 ± 0)", "60% (30 ± 0)", "80% (40 ± 0)"],
"marker": { "color": "red" },
"insidetextfont": { "color": "#333333" },
"yaxis": "y"
},
{
"visible": true,
"name": "b",
"mode": "lines+text",
"x": ["x1", "x2", "x3", "x4"],
"y": [100, 100, 100, 100],
"error_y": { "array": [0, 0, 0, 0], "color": "blue" },
"hoverinfo": "text+x+name",
"hover": [],
"text": ["80% (40 ± 0)", "60% (30 ± 0)", "40% (20 ± 0)", "20% (10 ± 0)"],
"marker": { "color": "blue" },
"insidetextfont": { "color": "#333333" },
"yaxis": "y"
}
]
}
}

View File

@@ -0,0 +1,79 @@
{
"input": {
"options": {
"globalSeriesType": "line",
"numberFormat": "0,0[.]00000",
"percentFormat": "0[.]00%",
"textFormat": "",
"showDataLabels": true,
"direction": { "type": "counterclockwise" },
"xAxis": { "type": "-", "labels": { "enabled": true } },
"yAxis": [
{ "type": "linear" },
{ "type": "linear", "opposite": true }
],
"series": { "stacking": null, "error_y": { "type": "data", "visible": true }, "percentValues": true },
"seriesOptions": {
"a": { "type": "line", "color": "red" },
"b": { "type": "line", "color": "blue" }
},
"columnMapping": {
"x": "x",
"y1": "y"
},
"missingValuesAsZero": true
},
"data": [
{
"name": "a",
"data": [
{ "x": "x1", "y": 10, "yError": 0 },
{ "x": "x2", "y": 20, "yError": 0 },
{ "x": "x3", "y": 30, "yError": 0 },
{ "x": "x4", "y": 40, "yError": 0 }
]
},
{
"name": "b",
"data": [
{ "x": "x1", "y": 40, "yError": 0 },
{ "x": "x2", "y": 30, "yError": 0 },
{ "x": "x3", "y": 20, "yError": 0 },
{ "x": "x4", "y": 10, "yError": 0 }
]
}
]
},
"output": {
"series": [
{
"visible": true,
"name": "a",
"mode": "lines+text",
"x": ["x1", "x2", "x3", "x4"],
"y": [20, 40, 60, 80],
"error_y": { "array": [0, 0, 0, 0], "color": "red" },
"hoverinfo": "text+x+name",
"hover": [],
"text": ["20% (10 ± 0)", "40% (20 ± 0)", "60% (30 ± 0)", "80% (40 ± 0)"],
"marker": { "color": "red" },
"insidetextfont": { "color": "#333333" },
"yaxis": "y"
},
{
"visible": true,
"name": "b",
"mode": "lines+text",
"x": ["x1", "x2", "x3", "x4"],
"y": [80, 60, 40, 20],
"error_y": { "array": [0, 0, 0, 0], "color": "blue" },
"hoverinfo": "text+x+name",
"hover": [],
"text": ["80% (40 ± 0)", "60% (30 ± 0)", "40% (20 ± 0)", "20% (10 ± 0)"],
"marker": { "color": "blue" },
"insidetextfont": { "color": "#333333" },
"yaxis": "y"
}
]
}
}

View File

@@ -0,0 +1,79 @@
{
"input": {
"options": {
"globalSeriesType": "line",
"numberFormat": "0,0[.]00000",
"percentFormat": "0[.]00%",
"textFormat": "",
"showDataLabels": true,
"direction": { "type": "counterclockwise" },
"xAxis": { "type": "-", "labels": { "enabled": true } },
"yAxis": [
{ "type": "linear" },
{ "type": "linear", "opposite": true }
],
"series": { "stacking": "stack", "error_y": { "type": "data", "visible": true } },
"seriesOptions": {
"a": { "type": "line", "color": "red" },
"b": { "type": "line", "color": "blue" }
},
"columnMapping": {
"x": "x",
"y1": "y"
},
"missingValuesAsZero": true
},
"data": [
{
"name": "a",
"data": [
{ "x": "x1", "y": 10, "yError": 0 },
{ "x": "x2", "y": 20, "yError": 0 },
{ "x": "x3", "y": 30, "yError": 0 },
{ "x": "x4", "y": 40, "yError": 0 }
]
},
{
"name": "b",
"data": [
{ "x": "x1", "y": 1, "yError": 0 },
{ "x": "x2", "y": 2, "yError": 0 },
{ "x": "x3", "y": 3, "yError": 0 },
{ "x": "x4", "y": 4, "yError": 0 }
]
}
]
},
"output": {
"series": [
{
"visible": true,
"name": "a",
"mode": "lines+text",
"x": ["x1", "x2", "x3", "x4"],
"y": [10, 20, 30, 40],
"error_y": { "array": [0, 0, 0, 0], "color": "red" },
"hoverinfo": "text+x+name",
"hover": [],
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
"marker": { "color": "red" },
"insidetextfont": { "color": "#333333" },
"yaxis": "y"
},
{
"visible": true,
"name": "b",
"mode": "lines+text",
"x": ["x1", "x2", "x3", "x4"],
"y": [11, 22, 33, 44],
"error_y": { "array": [0, 0, 0, 0], "color": "blue" },
"hoverinfo": "text+x+name",
"hover": [],
"text": ["1 ± 0", "2 ± 0", "3 ± 0", "4 ± 0"],
"marker": { "color": "blue" },
"insidetextfont": { "color": "#333333" },
"yaxis": "y"
}
]
}
}

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