mirror of
https://github.com/getredash/redash.git
synced 2025-12-19 17:37:19 -05:00
Compare commits
1 Commits
master
...
v8.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ac60585fa |
@@ -90,21 +90,6 @@ jobs:
|
||||
- run:
|
||||
name: Execute Cypress tests
|
||||
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:
|
||||
docker:
|
||||
- image: circleci/node:8
|
||||
@@ -130,16 +115,6 @@ workflows:
|
||||
- frontend-e2e-tests:
|
||||
requires:
|
||||
- frontend-lint
|
||||
- build-tarball:
|
||||
requires:
|
||||
- backend-unit-tests
|
||||
- frontend-unit-tests
|
||||
- frontend-e2e-tests
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /release\/.*/
|
||||
- build-docker-image:
|
||||
requires:
|
||||
- backend-unit-tests
|
||||
|
||||
113
CHANGELOG.md
113
CHANGELOG.md
@@ -1,5 +1,118 @@
|
||||
# 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
|
||||
|
||||
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).
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
[](https://redash.io/help/)
|
||||
[](https://datree.io/?src=badge)
|
||||

|
||||
[](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.
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -o errexit # fail the build if any task fails
|
||||
|
||||
flake8 --version ; pip --version
|
||||
# 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
|
||||
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||
|
||||
BIN
client/app/assets/images/db-logos/azure_kusto.png
Normal file
BIN
client/app/assets/images/db-logos/azure_kusto.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.0 KiB |
@@ -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
|
||||
.@{btn-prefix-cls} {
|
||||
transition-duration: 150ms;
|
||||
@@ -156,6 +150,10 @@
|
||||
border-color: transparent;
|
||||
color: @pagination-color;
|
||||
line-height: @pagination-item-size - 2px;
|
||||
|
||||
.@{pagination-prefix-cls}.mini & {
|
||||
line-height: @pagination-item-size-sm - 2px;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus .@{pagination-prefix-cls}-item-link,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
.alert {
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
padding: 15px;
|
||||
|
||||
span {
|
||||
cursor: pointer;
|
||||
|
||||
@@ -19,11 +19,15 @@ html, body {
|
||||
}
|
||||
|
||||
body {
|
||||
padding-top: @header-height;
|
||||
position: relative;
|
||||
&.headless {
|
||||
padding-top: 0;
|
||||
.nav.app-header {
|
||||
background: #F6F8F9;
|
||||
font-family: @redash-font;
|
||||
position: relative;
|
||||
|
||||
&.headless {
|
||||
padding-top: 10px;
|
||||
|
||||
.nav.app-header, .navbar {
|
||||
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 {
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
|
||||
}
|
||||
|
||||
.clickable {
|
||||
@@ -95,3 +123,150 @@ strong {
|
||||
resize: both !important;
|
||||
transition: height 0s, width 0s !important;
|
||||
}
|
||||
|
||||
// Ace Editor
|
||||
.ace_editor {
|
||||
border: 1px solid fade(@redash-gray, 15%) !important;
|
||||
}
|
||||
|
||||
.ace-tm {
|
||||
.ace_gutter {
|
||||
background: #fff !important;
|
||||
}
|
||||
|
||||
.ace_gutter-active-line {
|
||||
background-color: fade(@redash-gray, 20%) !important;
|
||||
}
|
||||
|
||||
.ace_marker-layer .ace_active-line {
|
||||
background: fade(@redash-gray, 9%) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
.collapsing,
|
||||
.collapse.in {
|
||||
padding: 5px 10px;
|
||||
padding: 0;
|
||||
transition: all 0.35s ease;
|
||||
}
|
||||
|
||||
|
||||
@@ -122,3 +122,21 @@
|
||||
top: 1px;
|
||||
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%);
|
||||
}
|
||||
@@ -55,14 +55,17 @@ textarea.v-resizable {
|
||||
.transition-duration(300ms);
|
||||
resize: none;
|
||||
box-shadow: 0 0 0 40px rgba(0, 0, 0, 0) !important;
|
||||
border-radius: 0;
|
||||
border-radius: @redash-input-radius;
|
||||
|
||||
&: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
|
||||
-----------------------------------------------------------*/
|
||||
|
||||
@@ -154,3 +154,9 @@
|
||||
Border Radius
|
||||
-----------------------------------------------------------*/
|
||||
.brd-2 { border-radius: 2px; }
|
||||
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Alignment
|
||||
-----------------------------------------------------------*/
|
||||
.va-top { vertical-align: top; }
|
||||
@@ -1,14 +1,37 @@
|
||||
.label {
|
||||
border-radius: 1px;
|
||||
padding: 4px 5px 3px;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
.label {
|
||||
border-radius: 2px;
|
||||
}
|
||||
padding: 3px 6px 4px;
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
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();
|
||||
}
|
||||
@@ -31,6 +31,17 @@ tags-list {
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.tags-list {
|
||||
.badge-light {
|
||||
background: fade(@redash-gray, 10%);
|
||||
color: fade(@redash-gray, 75%);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.max-character {
|
||||
.text-overflow();
|
||||
}
|
||||
@@ -45,6 +56,11 @@ tags-list {
|
||||
line-height: 100%;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
&.active, &.active:hover, &.active:focus {
|
||||
background-color: #fff;
|
||||
box-shadow: inset 3px 0px 0px @brand-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.list-group-item-heading {
|
||||
@@ -76,3 +92,18 @@ tags-list {
|
||||
height: 38px;
|
||||
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;
|
||||
}
|
||||
@@ -30,3 +30,266 @@ a.navbar-brand img {
|
||||
left: -9px;
|
||||
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%;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.popover {
|
||||
box-shadow: 0 2px 30px rgba(0, 0, 0, 0.2);
|
||||
box-shadow: fade(@redash-gray, 25%) 0px 0px 15px 0px;
|
||||
}
|
||||
|
||||
.popover-title {
|
||||
|
||||
@@ -132,9 +132,15 @@
|
||||
}
|
||||
|
||||
.tab-nav {
|
||||
margin-bottom: 0px;
|
||||
|
||||
> li.rd-tab-btn {
|
||||
float: right;
|
||||
padding-right: 10px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
> li > a {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,3 +97,53 @@
|
||||
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;
|
||||
}
|
||||
@@ -2,7 +2,8 @@
|
||||
background-color: #fff;
|
||||
margin-bottom: @grid-gutter-width;
|
||||
position: relative;
|
||||
box-shadow: @tile-shadow;
|
||||
border-radius: 3px;
|
||||
box-shadow: fade(@redash-gray, 15%) 0px 4px 9px -3px;
|
||||
|
||||
&[class*="bg-"] {
|
||||
color: #fff;
|
||||
@@ -12,6 +13,10 @@
|
||||
margin-bottom: @grid-gutter-width/2;
|
||||
}
|
||||
}
|
||||
.tiled {
|
||||
border-radius: 3px;
|
||||
box-shadow: fade(@redash-gray, 15%) 0px 4px 9px -3px;
|
||||
}
|
||||
|
||||
.t-header {
|
||||
.th-title {
|
||||
@@ -74,6 +79,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
.t-header:not(.th-alt) {
|
||||
padding: 15px;
|
||||
|
||||
ul {
|
||||
margin-bottom: 0;
|
||||
line-height: 2.2;
|
||||
}
|
||||
}
|
||||
|
||||
.tb-padding {
|
||||
padding: 20px 23px 30px;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
@logo-height: @header-height;
|
||||
@boxed-width: 1170px;
|
||||
@body-bg: #edecec;
|
||||
@spacing: 15px;
|
||||
@redash-radius: 3px;
|
||||
|
||||
|
||||
/* --------------------------------------------------------
|
||||
@@ -39,6 +41,7 @@
|
||||
-----------------------------------------------------------*/
|
||||
@font-icon: 'Material-Design-Iconic-Font';
|
||||
@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;
|
||||
|
||||
|
||||
@@ -59,6 +62,7 @@
|
||||
@input-border: #e8e8e8;
|
||||
@input-border-radius: 0;
|
||||
@input-border-radius-large: 0px;
|
||||
@redash-input-radius: 2px;
|
||||
@input-height-large: 40px;
|
||||
@input-height-base: 35px;
|
||||
@input-height-small: 30px;
|
||||
@@ -94,6 +98,11 @@
|
||||
@gray-light: #828282;
|
||||
@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 **/
|
||||
@state-success-text: @green;
|
||||
@state-info-text: @blue;
|
||||
@@ -192,7 +201,6 @@
|
||||
@pagination-hover-color: #333;
|
||||
@pagination-hover-bg: #d7d7d7;
|
||||
@pagination-hover-border: @pagination-border;
|
||||
@pager-border-radius: 5px;
|
||||
|
||||
|
||||
/* --------------------------------------------------------
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
@import 'inc/progress-bar';
|
||||
@import 'inc/widgets';
|
||||
@import 'inc/table';
|
||||
@import 'inc/pagination';
|
||||
@import 'inc/alert';
|
||||
@import 'inc/media';
|
||||
@import 'inc/modal';
|
||||
@@ -54,11 +53,9 @@
|
||||
@import 'inc/schema-browser';
|
||||
@import 'inc/toast';
|
||||
@import 'inc/visualizations/box';
|
||||
@import 'inc/visualizations/counter-render';
|
||||
@import 'inc/visualizations/sankey';
|
||||
@import 'inc/visualizations/pivot-table';
|
||||
@import 'inc/visualizations/map';
|
||||
@import 'inc/visualizations/chart';
|
||||
@import 'inc/visualizations/sunburst';
|
||||
@import 'inc/visualizations/cohort';
|
||||
@import 'inc/visualizations/misc';
|
||||
@@ -71,11 +68,11 @@
|
||||
@import 'inc/vendor-overrides/ui-select';
|
||||
|
||||
/** REDASH STYLING **/
|
||||
@import 'redash/redash-newstyle';
|
||||
@import 'redash/redash-table';
|
||||
@import 'redash/query';
|
||||
@import 'redash/tags-control';
|
||||
@import 'redash/css-logo';
|
||||
@import 'redash/loading-indicator';
|
||||
|
||||
|
||||
|
||||
|
||||
51
client/app/assets/less/redash/loading-indicator.less
Normal file
51
client/app/assets/less/redash/loading-indicator.less
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -92,7 +92,7 @@ edit-in-place p.editable:hover {
|
||||
}
|
||||
|
||||
.filter-container {
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.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 {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
}
|
||||
|
||||
.embed-heading {
|
||||
h3 {
|
||||
line-height: 1.75;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.widget-wrapper {
|
||||
.body-container {
|
||||
.filters-wrapper {
|
||||
@@ -343,7 +343,8 @@ a.label-tag {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -676,8 +677,17 @@ nav .rg-bottom {
|
||||
.filter-container {
|
||||
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
@@ -19,8 +19,6 @@
|
||||
@import 'inc/ie-warning';
|
||||
@import 'inc/flex';
|
||||
|
||||
@import 'redash/redash-newstyle';
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
|
||||
83
client/app/components/BeaconConsent.jsx
Normal file
83
client/app/components/BeaconConsent.jsx
Normal 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;
|
||||
@@ -1,23 +1,12 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
// ANGULAR_REMOVE_ME
|
||||
import { react2angular } from 'react2angular';
|
||||
|
||||
import ColorPicker from '@/components/ColorPicker';
|
||||
|
||||
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) {
|
||||
ngModule.component('colorBox', react2angular(ColorBox));
|
||||
ngModule.component('colorBox', react2angular(ColorPicker.Swatch));
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
|
||||
93
client/app/components/ColorPicker/Input.jsx
Normal file
93
client/app/components/ColorPicker/Input.jsx
Normal 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: () => {},
|
||||
};
|
||||
37
client/app/components/ColorPicker/Swatch.jsx
Normal file
37
client/app/components/ColorPicker/Swatch.jsx
Normal 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,
|
||||
};
|
||||
128
client/app/components/ColorPicker/index.jsx
Normal file
128
client/app/components/ColorPicker/index.jsx
Normal 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;
|
||||
40
client/app/components/ColorPicker/index.less
Normal file
40
client/app/components/ColorPicker/index.less
Normal 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;
|
||||
}
|
||||
19
client/app/components/ColorPicker/input.less
Normal file
19
client/app/components/ColorPicker/input.less
Normal 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;
|
||||
}
|
||||
30
client/app/components/ColorPicker/swatch.less
Normal file
30
client/app/components/ColorPicker/swatch.less
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -153,6 +153,7 @@ function EditParameterSettingsDialog(props) {
|
||||
<Input
|
||||
value={isNull(param.title) ? getDefaultTitle(param.name) : param.title}
|
||||
onChange={e => setParam({ ...param, title: e.target.value })}
|
||||
data-test="ParameterTitleInput"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="Type" {...formItemProps}>
|
||||
@@ -176,7 +177,7 @@ function EditParameterSettingsDialog(props) {
|
||||
</Select>
|
||||
</Form.Item>
|
||||
{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
|
||||
rows={3}
|
||||
value={param.enumOptions}
|
||||
|
||||
@@ -32,6 +32,10 @@ export const TYPES = {
|
||||
'/user-guide/users/authentication-options',
|
||||
'Guide: Authentication Options',
|
||||
],
|
||||
USAGE_DATA_SHARING: [
|
||||
'/open-source/admin-guide/usage-data',
|
||||
'Help: Anonymous Usage Data Sharing',
|
||||
],
|
||||
DS_ATHENA: [
|
||||
'/data-sources/amazon-athena-setup',
|
||||
'Guide: Help Setting up Amazon Athena',
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { react2angular } from 'react2angular';
|
||||
import Button from 'antd/lib/button';
|
||||
import Badge from 'antd/lib/badge';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import { KeyboardShortcuts } from '@/services/keyboard-shortcuts';
|
||||
|
||||
function ParameterApplyButton({ paramCount, onClick, isApplying }) {
|
||||
// show spinner when applying (also when count is empty so the fade out is consistent)
|
||||
const icon = isApplying || !paramCount ? 'spinner fa-pulse' : 'check';
|
||||
function ParameterApplyButton({ paramCount, onClick }) {
|
||||
// show spinner when count is empty so the fade out is consistent
|
||||
const icon = !paramCount ? 'spinner fa-pulse' : 'check';
|
||||
|
||||
return (
|
||||
<div className="parameter-apply-button" data-show={!!paramCount} data-test="ParameterApplyButton">
|
||||
@@ -28,11 +27,6 @@ function ParameterApplyButton({ paramCount, onClick, isApplying }) {
|
||||
ParameterApplyButton.propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
paramCount: PropTypes.number.isRequired,
|
||||
isApplying: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('parameterApplyButton', react2angular(ParameterApplyButton));
|
||||
}
|
||||
|
||||
init.init = true;
|
||||
export default ParameterApplyButton;
|
||||
|
||||
@@ -14,7 +14,7 @@ import Input from 'antd/lib/input';
|
||||
import Radio from 'antd/lib/radio';
|
||||
import Form from 'antd/lib/form';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import { ParameterValueInput } from '@/components/ParameterValueInput';
|
||||
import ParameterValueInput from '@/components/ParameterValueInput';
|
||||
import { ParameterMappingType } from '@/services/widget';
|
||||
import { Parameter } from '@/services/query';
|
||||
import { HelpTrigger } from '@/components/HelpTrigger';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { react2angular } from 'react2angular';
|
||||
import Select from 'antd/lib/select';
|
||||
import Input from 'antd/lib/input';
|
||||
import InputNumber from 'antd/lib/input-number';
|
||||
@@ -19,7 +18,7 @@ const multipleValuesProps = {
|
||||
maxTagPlaceholder: num => `+${num.length} more`,
|
||||
};
|
||||
|
||||
export class ParameterValueInput extends React.Component {
|
||||
class ParameterValueInput extends React.Component {
|
||||
static propTypes = {
|
||||
type: PropTypes.string,
|
||||
value: PropTypes.any, // eslint-disable-line react/forbid-prop-types
|
||||
@@ -108,7 +107,6 @@ export class ParameterValueInput extends React.Component {
|
||||
value={value}
|
||||
onChange={this.onSelect}
|
||||
dropdownMatchSelectWidth={false}
|
||||
dropdownClassName="ant-dropdown-in-bootstrap-modal"
|
||||
showSearch
|
||||
showArrow
|
||||
style={{ minWidth: 60 }}
|
||||
@@ -142,7 +140,7 @@ export class ParameterValueInput extends React.Component {
|
||||
const { className } = this.props;
|
||||
const { value } = this.state;
|
||||
|
||||
const normalize = val => !isNaN(val) && val || 0;
|
||||
const normalize = val => (isNaN(val) ? undefined : val);
|
||||
|
||||
return (
|
||||
<InputNumber
|
||||
@@ -194,34 +192,4 @@ export class ParameterValueInput extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default function init(ngModule) {
|
||||
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;
|
||||
export default ParameterValueInput;
|
||||
|
||||
@@ -5,9 +5,15 @@
|
||||
.parameter-input {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
.@{ant-prefix}-input[type="text"] {
|
||||
width: 195px;
|
||||
.@{ant-prefix}-input,
|
||||
.@{ant-prefix}-input-number {
|
||||
min-width: 100% !important;
|
||||
}
|
||||
|
||||
.@{ant-prefix}-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&[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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
208
client/app/components/Parameters.jsx
Normal file
208
client/app/components/Parameters.jsx
Normal 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;
|
||||
124
client/app/components/Parameters.less
Normal file
124
client/app/components/Parameters.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,6 @@ export class QueryBasedParameterInput extends React.Component {
|
||||
value={isArray(value) ? value : toString(value)}
|
||||
onChange={onSelect}
|
||||
dropdownMatchSelectWidth={false}
|
||||
dropdownClassName="ant-dropdown-in-bootstrap-modal"
|
||||
optionFilterProp="children"
|
||||
showSearch
|
||||
showArrow
|
||||
|
||||
@@ -4,6 +4,8 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import EmptyState from '@/components/items-list/components/EmptyState';
|
||||
|
||||
import './CardsList.less';
|
||||
|
||||
const { Search } = Input;
|
||||
|
||||
export default class CardsList extends React.Component {
|
||||
|
||||
76
client/app/components/cards-list/CardsList.less
Normal file
76
client/app/components/cards-list/CardsList.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,10 @@
|
||||
@import '../assets/less/inc/variables';
|
||||
|
||||
// ANGULAR_REMOVE_ME
|
||||
color-box {
|
||||
vertical-align: text-bottom;
|
||||
display: inline;
|
||||
|
||||
span {
|
||||
width: 12px !important;
|
||||
height: 12px !important;
|
||||
display: inline-block !important;
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
vertical-align: middle;
|
||||
border: 1px solid rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
& ~ span {
|
||||
vertical-align: bottom;
|
||||
color: @input-color;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<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 class="modal-footer">
|
||||
<button type="button" class="btn btn-default" ng-click="$ctrl.dismiss()">Close</button>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
visualization="$ctrl.widget.visualization"
|
||||
query-result="$ctrl.widget.getQueryResult()"
|
||||
filters="$ctrl.filters"
|
||||
context="'widget'"
|
||||
></visualization-renderer>
|
||||
</div>
|
||||
<div ng-switch-default class="body-row-auto spinner-container">
|
||||
@@ -64,7 +65,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="body-row clearfix tile__bottom-control">
|
||||
<div class="body-row tile__bottom-control">
|
||||
<span>
|
||||
<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">
|
||||
<i class="zmdi zmdi-refresh" ng-class="{ 'zmdi-hc-spin': $ctrl.refreshClickButtonId === 1}"></i>
|
||||
<span am-time-ago="$ctrl.widget.getQueryResult().getUpdatedAt()"></span>
|
||||
@@ -75,11 +77,14 @@
|
||||
<span class="visible-print">
|
||||
<i class="zmdi zmdi-time-restore"></i> {{$ctrl.widget.getQueryResult().getUpdatedAt() | dateTime}}
|
||||
</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>
|
||||
<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 class="btn btn-sm btn-default hidden-print btn-transparent btn__refresh" ng-click="$ctrl.refresh(2)" ng-if="!$ctrl.public">
|
||||
<i class="zmdi zmdi-refresh" ng-class="{ 'zmdi-hc-spin': $ctrl.refreshClickButtonId === 2}"></i>
|
||||
</button>
|
||||
<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>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -92,6 +92,8 @@ function DashboardWidgetCtrl($scope, $location, $uibModal, $window, $rootScope,
|
||||
return this.widget.load(refresh, maxAge);
|
||||
};
|
||||
|
||||
this.forceRefresh = () => this.load(true);
|
||||
|
||||
this.refresh = (buttonId) => {
|
||||
this.refreshClickButtonId = buttonId;
|
||||
this.load(true).finally(() => {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@import '../../assets/less/inc/variables';
|
||||
|
||||
.tile .t-header .th-title a.query-link {
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
@@ -26,6 +28,10 @@ visualization-name {
|
||||
}
|
||||
|
||||
.widget-wrapper {
|
||||
.parameter-container {
|
||||
margin: 0 15px;
|
||||
}
|
||||
|
||||
.body-container {
|
||||
display: flex;
|
||||
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-item {
|
||||
|
||||
@@ -72,6 +72,7 @@ Columns.custom.sortable = sortable;
|
||||
|
||||
export default class ItemsTable extends React.Component {
|
||||
static propTypes = {
|
||||
loading: PropTypes.bool,
|
||||
// eslint-disable-next-line react/forbid-prop-types
|
||||
items: PropTypes.arrayOf(PropTypes.object),
|
||||
columns: PropTypes.arrayOf(PropTypes.shape({
|
||||
@@ -89,6 +90,7 @@ export default class ItemsTable extends React.Component {
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
loading: false,
|
||||
items: [],
|
||||
columns: [],
|
||||
showHeader: true,
|
||||
@@ -150,6 +152,7 @@ export default class ItemsTable extends React.Component {
|
||||
return (
|
||||
<Table
|
||||
className={classNames('table-data', { 'ant-table-headerless': !showHeader })}
|
||||
loading={this.props.loading}
|
||||
columns={columns}
|
||||
showHeader={showHeader}
|
||||
dataSource={rows}
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
@@ -20,21 +20,21 @@
|
||||
<div class="alert alert-danger" data-test="ErrorMessage">Error: {{$ctrl.error}}</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>
|
||||
</div>
|
||||
|
||||
<div class="clearfix tile__bottom-control">
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<div class="tile__bottom-control">
|
||||
<span>
|
||||
<a class="small hidden-print" ng-click="$ctrl.refreshQueryResults()">
|
||||
<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>
|
||||
<rd-timer from="$ctrl.refreshStartedAt" ng-if="$ctrl.loading"></rd-timer>
|
||||
</a>
|
||||
<span class="small visible-print"><i class="zmdi zmdi-time-restore"></i> {{$ctrl.queryResult.getUpdatedAt() | dateTime}} UTC</span>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right hidden-print" ng-if="!$ctrl.hideQueryLink">
|
||||
</span>
|
||||
|
||||
<span class="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>
|
||||
@@ -57,7 +57,6 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -64,15 +64,6 @@ export function createNumberFormatter(format) {
|
||||
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) {
|
||||
if (!isString(str)) {
|
||||
return '';
|
||||
|
||||
@@ -137,8 +137,16 @@ function DashboardCtrl(
|
||||
this.extractGlobalParameters();
|
||||
});
|
||||
|
||||
const collectFilters = (dashboard, forceRefresh) => {
|
||||
const queryResultPromises = _.compact(this.dashboard.widgets.map((widget) => {
|
||||
const collectFilters = (dashboard, forceRefresh, updatedParameters = []) => {
|
||||
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
|
||||
return widget.load(forceRefresh);
|
||||
}));
|
||||
@@ -202,9 +210,9 @@ function DashboardCtrl(
|
||||
|
||||
this.loadDashboard();
|
||||
|
||||
this.refreshDashboard = () => {
|
||||
this.refreshDashboard = (parameters) => {
|
||||
this.refreshInProgress = true;
|
||||
collectFilters(this.dashboard, true).finally(() => {
|
||||
collectFilters(this.dashboard, true, parameters).finally(() => {
|
||||
this.refreshInProgress = false;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@import '../../assets/less/inc/variables';
|
||||
|
||||
.dashboard-wrapper {
|
||||
flex-grow: 1;
|
||||
margin-bottom: 85px;
|
||||
@@ -20,7 +22,8 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.pivot-table-renderer > table, grid-renderer > div, visualization-renderer > div {
|
||||
.pivot-table-renderer > table,
|
||||
visualization-renderer > .visualization-renderer-wrapper {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
@@ -51,13 +54,6 @@
|
||||
background-size: calc((100vw - 15px) / 6) 5px;
|
||||
background-position: -7px 1px;
|
||||
}
|
||||
|
||||
.widget-menu-regular {
|
||||
display: none;
|
||||
}
|
||||
.widget-menu-remove {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-widget-wrapper:not(.widget-auto-height-enabled) {
|
||||
@@ -70,7 +66,7 @@
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
> div {
|
||||
> .visualization-renderer-wrapper {
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
}
|
||||
@@ -85,7 +81,7 @@
|
||||
.map-visualization-container,
|
||||
.word-cloud-visualization-container,
|
||||
.box-plot-deprecated-visualization-container,
|
||||
.plotly-chart-container {
|
||||
.chart-visualization-container {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
@@ -96,7 +92,7 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
counter {
|
||||
.counter-visualization-content {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 15px;
|
||||
@@ -126,6 +122,15 @@
|
||||
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 {
|
||||
.tags-control a {
|
||||
opacity: 0;
|
||||
@@ -140,6 +145,8 @@
|
||||
}
|
||||
|
||||
.dashboard__control {
|
||||
margin: 8px 0;
|
||||
|
||||
.save-status {
|
||||
vertical-align: middle;
|
||||
margin-right: 7px;
|
||||
@@ -234,3 +241,40 @@ dashboard-grid {
|
||||
display: flex;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,24 @@
|
||||
<div class="container">
|
||||
<div 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
|
||||
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 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
|
||||
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>
|
||||
<empty-state
|
||||
title="'Welcome to Redash 👋'"
|
||||
@@ -31,13 +46,19 @@
|
||||
</p>
|
||||
|
||||
<div class="list-group">
|
||||
<a ng-href="dashboard/{{dashboard.slug}}" class="list-group-item" ng-repeat="dashboard in $ctrl.favoriteDashboards"
|
||||
ng-if="dashboard.is_favorite">
|
||||
<a
|
||||
ng-href="dashboard/{{ dashboard.slug }}"
|
||||
class="list-group-item"
|
||||
ng-repeat="dashboard in $ctrl.favoriteDashboards"
|
||||
ng-if="dashboard.is_favorite"
|
||||
>
|
||||
<span class="btn-favourite">
|
||||
<i class="fa fa-star" aria-hidden="true"></i>
|
||||
</span>
|
||||
{{dashboard.name}}
|
||||
<span class="label label-default" ng-if="dashboard.is_draft">Unpublished</span>
|
||||
{{ dashboard.name }}
|
||||
<span class="label label-default" ng-if="dashboard.is_draft"
|
||||
>Unpublished</span
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,17 +72,24 @@
|
||||
Favorite <a href="queries">Queries</a> will appear here
|
||||
</p>
|
||||
<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">
|
||||
<i class="fa fa-star" aria-hidden="true"></i>
|
||||
</span>
|
||||
{{query.name}}
|
||||
<span class="label label-default" ng-if="query.is_draft">Unpublished</span>
|
||||
{{ query.name }}
|
||||
<span class="label label-default" ng-if="query.is_draft"
|
||||
>Unpublished</span
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<beacon-consent></beacon-consent>
|
||||
</div>
|
||||
|
||||
@@ -85,9 +85,12 @@
|
||||
<div class="editor__left__data-source">
|
||||
<ui-select ng-model="query.data_source_id" remove-selected="false" ng-disabled="!isQueryOwner || !sourceMode"
|
||||
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">
|
||||
{{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>
|
||||
</div>
|
||||
@@ -191,8 +194,8 @@
|
||||
<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="p-t-15 p-b-5" ng-if="query.hasParameters()">
|
||||
<parameters parameters="query.getParametersDefs()" sync-values="!query.isNew()" editable="sourceMode && canEdit"
|
||||
on-updated="onParametersUpdated" on-values-change="executeQuery"></parameters>
|
||||
<parameters parameters="query.getParametersDefs()" editable="sourceMode && canEdit" disable-url-update="query.isNew()"
|
||||
on-values-change="executeQuery" on-pending-values-change="applyParametersChanges" on-parameters-edit="onParametersUpdated"></parameters>
|
||||
</div>
|
||||
<!-- Query Execution Status -->
|
||||
|
||||
@@ -240,7 +243,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
<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>
|
||||
|
||||
@@ -215,6 +215,10 @@ function QueryViewCtrl(
|
||||
|
||||
$scope.loadTags = () => getTags('api/queries/tags').then(tags => map(tags, t => t.name));
|
||||
|
||||
$scope.applyParametersChanges = () => {
|
||||
$scope.$apply();
|
||||
};
|
||||
|
||||
$scope.saveQuery = (customOptions, data) => {
|
||||
let request = data;
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ class QuerySnippetsList extends React.Component {
|
||||
There are no query snippets yet.
|
||||
{policy.isCreateQuerySnippetEnabled() && (
|
||||
<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>
|
||||
|
||||
@@ -10,13 +10,14 @@ import Select from 'antd/lib/select';
|
||||
import Checkbox from 'antd/lib/checkbox';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import LoadingState from '@/components/items-list/components/LoadingState';
|
||||
import { HelpTrigger } from '@/components/HelpTrigger';
|
||||
|
||||
import { routesToAngularRoutes } from '@/lib/utils';
|
||||
import { clientConfig } from '@/services/auth';
|
||||
import settingsMenu from '@/services/settingsMenu';
|
||||
import recordEvent from '@/services/recordEvent';
|
||||
import OrgSettings from '@/services/organizationSettings';
|
||||
import { HelpTrigger } from '@/components/HelpTrigger';
|
||||
import DynamicComponent from '@/components/DynamicComponent';
|
||||
|
||||
const Option = Select.Option;
|
||||
|
||||
@@ -155,23 +156,6 @@ class OrganizationSettings extends React.Component {
|
||||
))}
|
||||
</Select>
|
||||
</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">
|
||||
<Checkbox
|
||||
name="feature_show_permissions_control"
|
||||
@@ -181,6 +165,34 @@ class OrganizationSettings extends React.Component {
|
||||
Enable experimental multiple owners support
|
||||
</Checkbox>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,10 +3,13 @@ import notification from '@/services/notification';
|
||||
|
||||
export default {
|
||||
get: () => $http.get('api/settings/organization').then(response => response.data),
|
||||
save: data => $http.post('api/settings/organization', data).then((response) => {
|
||||
notification.success('Settings changes saved.');
|
||||
save: (data, message = 'Settings changes saved.') => $http
|
||||
.post('api/settings/organization', data)
|
||||
.then((response) => {
|
||||
notification.success(message);
|
||||
return response.data;
|
||||
}).catch(() => {
|
||||
})
|
||||
.catch(() => {
|
||||
notification.error('Failed saving changes.');
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -93,6 +93,10 @@ function collectParams(parts) {
|
||||
return parameters;
|
||||
}
|
||||
|
||||
function isEmptyValue(value) {
|
||||
return isNull(value) || isUndefined(value) || (value === '') || (isArray(value) && value.length === 0);
|
||||
}
|
||||
|
||||
function isDateParameter(paramType) {
|
||||
return includes(['date', 'datetime-local', 'datetime-with-seconds'], paramType);
|
||||
}
|
||||
@@ -164,10 +168,6 @@ export class Parameter {
|
||||
return isNull(this.getValue());
|
||||
}
|
||||
|
||||
getValue(extra = {}) {
|
||||
return this.constructor.getValue(this, extra);
|
||||
}
|
||||
|
||||
get hasDynamicValue() {
|
||||
if (isDateParameter(this.type)) {
|
||||
return isDynamicDate(this.value);
|
||||
@@ -188,9 +188,12 @@ export class Parameter {
|
||||
return false;
|
||||
}
|
||||
|
||||
getValue(extra = {}) {
|
||||
return this.constructor.getValue(this, extra);
|
||||
}
|
||||
|
||||
static getValue(param, extra = {}) {
|
||||
const { value, type, useCurrentDateTime, multiValuesOptions } = param;
|
||||
const isEmptyValue = isNull(value) || isUndefined(value) || (value === '') || (isArray(value) && value.length === 0);
|
||||
if (isDateRangeParameter(type) && param.hasDynamicValue) {
|
||||
const { dynamicValue } = param;
|
||||
if (dynamicValue) {
|
||||
@@ -211,7 +214,7 @@ export class Parameter {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isEmptyValue) {
|
||||
if (isEmptyValue(value)) {
|
||||
// keep support for existing useCurentDateTime (not available in UI)
|
||||
if (
|
||||
includes(['date', 'datetime-local', 'datetime-with-seconds'], type) &&
|
||||
@@ -325,7 +328,11 @@ export class Parameter {
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -417,7 +424,7 @@ class Parameters {
|
||||
const fallback = () => map(this.query.options.parameters, i => i.name);
|
||||
|
||||
let parameters = [];
|
||||
if (this.query.query) {
|
||||
if (this.query.query !== undefined) {
|
||||
try {
|
||||
const parts = Mustache.parse(this.query.query);
|
||||
parameters = uniq(collectParams(parts));
|
||||
|
||||
@@ -191,6 +191,7 @@ function EditVisualizationDialog({ dialog, visualization, query, queryResult })
|
||||
options={options}
|
||||
visualizationName={name}
|
||||
onOptionsChange={onOptionsChanged}
|
||||
context="query"
|
||||
/>
|
||||
</div>
|
||||
</Grid.Col>
|
||||
|
||||
@@ -60,12 +60,13 @@ export function VisualizationRenderer(props) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{showFilters && <Filters filters={filters} onChange={setFilters} />}
|
||||
<div>
|
||||
<div className="visualization-renderer-wrapper">
|
||||
<Renderer
|
||||
key={`visualization${visualization.id}`}
|
||||
options={options}
|
||||
data={filteredData}
|
||||
visualizationName={visualization.name}
|
||||
context={props.context}
|
||||
/>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
@@ -77,6 +78,7 @@ VisualizationRenderer.propTypes = {
|
||||
queryResult: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
filters: FiltersType,
|
||||
showFilters: PropTypes.bool,
|
||||
context: PropTypes.oneOf(['query', 'widget']).isRequired,
|
||||
};
|
||||
|
||||
VisualizationRenderer.defaultProps = {
|
||||
|
||||
@@ -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;
|
||||
51
client/app/visualizations/chart/Renderer/PlotlyChart.jsx
Normal file
51
client/app/visualizations/chart/Renderer/PlotlyChart.jsx
Normal 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;
|
||||
16
client/app/visualizations/chart/Renderer/index.jsx
Normal file
16
client/app/visualizations/chart/Renderer/index.jsx
Normal 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;
|
||||
@@ -1,4 +1,4 @@
|
||||
.plotly-chart-container {
|
||||
.chart-visualization-container {
|
||||
height: 400px;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -168,13 +168,19 @@
|
||||
<input type="checkbox" ng-model="$ctrl.options.series.percentValues"> Normalize values to percentage
|
||||
</label>
|
||||
</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 ng-if="$ctrl.options.globalSeriesType == 'custom'">
|
||||
<div class="form-group">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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" } }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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 } }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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" } }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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 } }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -26,12 +26,11 @@ export default function getChartData(data, options) {
|
||||
let sizeValue = null;
|
||||
let zValue = null;
|
||||
|
||||
forOwn(row, (v, definition) => {
|
||||
forOwn(row, (value, definition) => {
|
||||
definition = '' + definition;
|
||||
const definitionParts = definition.split('::') || definition.split('__');
|
||||
const name = definitionParts[0];
|
||||
const type = mappings ? mappings[definition] : definitionParts[1];
|
||||
let value = v;
|
||||
|
||||
if (type === 'unused') {
|
||||
return;
|
||||
@@ -42,9 +41,6 @@ export default function getChartData(data, options) {
|
||||
point[type] = value;
|
||||
}
|
||||
if (type === 'y') {
|
||||
if (value == null) {
|
||||
value = 0;
|
||||
}
|
||||
yValues[name] = value;
|
||||
point[type] = value;
|
||||
}
|
||||
|
||||
32
client/app/visualizations/chart/getChartData.test.js
Normal file
32
client/app/visualizations/chart/getChartData.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -6,9 +6,10 @@ import { registerVisualization } from '@/visualizations';
|
||||
import { clientConfig } from '@/services/auth';
|
||||
import ColorPalette from '@/visualizations/ColorPalette';
|
||||
import getChartData from './getChartData';
|
||||
import template from './chart.html';
|
||||
import editorTemplate from './chart-editor.html';
|
||||
|
||||
import Renderer from './Renderer';
|
||||
|
||||
const DEFAULT_OPTIONS = {
|
||||
globalSeriesType: 'column',
|
||||
sortX: true,
|
||||
@@ -27,6 +28,8 @@ const DEFAULT_OPTIONS = {
|
||||
percentFormat: '0[.]00%',
|
||||
// dateTimeFormat: 'DD/MM/YYYY HH:mm', // will be set from clientConfig
|
||||
textFormat: '', // default: combination of {{ @@yPercent }} ({{ @@y }} ± {{ @@yError }})
|
||||
|
||||
missingValuesAsZero: true,
|
||||
};
|
||||
|
||||
function initEditorForm(options, columns) {
|
||||
@@ -69,26 +72,6 @@ function initEditorForm(options, columns) {
|
||||
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 = {
|
||||
template: editorTemplate,
|
||||
bindings: {
|
||||
@@ -304,7 +287,6 @@ const ChartEditor = {
|
||||
};
|
||||
|
||||
export default function init(ngModule) {
|
||||
ngModule.component('chartRenderer', ChartRenderer);
|
||||
ngModule.component('chartEditor', ChartEditor);
|
||||
|
||||
ngModule.run(($injector) => {
|
||||
@@ -312,11 +294,21 @@ export default function init(ngModule) {
|
||||
type: 'CHART',
|
||||
name: 'Chart',
|
||||
isDefault: true,
|
||||
getOptions: options => merge({}, DEFAULT_OPTIONS, {
|
||||
getOptions: (options) => {
|
||||
const result = merge({}, DEFAULT_OPTIONS, {
|
||||
showDataLabels: options.globalSeriesType === 'pie',
|
||||
dateTimeFormat: clientConfig.dateTimeFormat,
|
||||
}, options),
|
||||
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),
|
||||
|
||||
defaultColumns: 3,
|
||||
|
||||
100
client/app/visualizations/chart/plotly/applyLayoutFixes.js
Normal file
100
client/app/visualizations/chart/plotly/applyLayoutFixes.js
Normal 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']));
|
||||
}
|
||||
}
|
||||
40
client/app/visualizations/chart/plotly/customChartUtils.js
Normal file
40
client/app/visualizations/chart/plotly/customChartUtils.js
Normal 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
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user