From a1c00517321b4759223e70959324950709e41bfc Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Fri, 10 May 2019 12:57:08 -0700 Subject: [PATCH] Merge master into lyft-release-sp8 (#7484) * filter out all nan series (#7313) * improve not rich tooltip (#7345) * Create issue_label_bot.yaml (#7341) * fix: do not save colors without a color scheme (#7347) * [wtforms] Strip leading/trailing whitespace (#7084) * [schema] Updating the datasources schema (#5451) * limit tables/views returned if schema is not provided (#7358) * limit tables/views returned if schema is not provided * fix typo * improve code performance * handle the case when table name or view name does not present a schema * Add type anno (#7342) * Updated local dev instructions to include missing step * First pass at type annotations * [schema] Updating the base column schema (#5452) * Update 937d04c16b64_update_datasources.py (#7361) * Feature flag for client cache (#7348) * Feature flag for client cache * Fix integration test * Revert "Fix integration test" This reverts commit 58434ab98a015d6e96db4a97f26255aa282d989d. * Feature flag for client cache * Fix integration tests * Add feature flag to config.py * Add another feature check * Fix more integration tests * Fix raw HTML in SliceAdder (#7338) * remove backendSync.json (#7331) * [bubbles] issue when using duplicated metrics (#7087) * SUPERSET-7: Docker compose config version breaks on Ubuntu 16.04 (#7359) * SUPERSET-8: Update text in docs copyright footer (#7360) * SUPERSET-7: Docker compose config version breaks on Ubuntu 16.04 * SUPERSET-8: Extra text in docs copyright footer * [schema] Adding commits and removing unnecessary foreign-key definitions (#7371) * Store last selected dashboard in sessionStorage (#7181) * Store last selected dashboard in sessionStorage * Fix tests * [schema] Updating the base metric schema (#5453) * Fix NoneType bug & fill the test recipients with original recipients if empty (#7365) * Added living goods as among the users of Superset (#7407) * Added living goods as among the users of Superset Living Goods is a non profit organisation with operation in africa and the middle east. We work in community health use data heavily on day to day. Superset is our platform of choice for dashboards. * Update README.md * [dashboard] allow user re-order top-level tabs (#7390) * [SQL Lab] Increase timeout threshold for offline check (#7411) * Bump FAB to 2.0.0 (#7323) * Bump FAB to 2.0.0 * [tests] whitelist SecurityApi login and refresh endpoints * [style] Fix, C812 missing trailing commas * [security] Remove SUPERSET_UPDATE_PERMS flag Registering sources needs to be performed after the views are initialized on UPDATE_PERMS=False configuration * [docs] New, FAB_UPDATE_PERMS and flask fab cli * [docs] Fix, db upgrade needs to come first, create-admin needs a db * [cli] New, superset init bootstraps all permissions for FAB and Superset * [style] Fix, flakes * [annotations] Improves UX on annotation validation, start_dttm, end_dttm (#7326) * Setting renderTrigger on label_colors (#7410) * Refactor out controlUtils.js module + unit tests (#7350) * [WiP]refactor out a controlUtils.js file * unit tests * add missing license * Addressing comments * feature: see Presto row and array data types (#7413) * Merge lastest from master into lyft-release-sp8 (#7405) * filter out all nan series (#7313) * improve not rich tooltip (#7345) * Create issue_label_bot.yaml (#7341) * fix: do not save colors without a color scheme (#7347) * [wtforms] Strip leading/trailing whitespace (#7084) * [schema] Updating the datasources schema (#5451) * limit tables/views returned if schema is not provided (#7358) * limit tables/views returned if schema is not provided * fix typo * improve code performance * handle the case when table name or view name does not present a schema * Add type anno (#7342) * Updated local dev instructions to include missing step * First pass at type annotations * [schema] Updating the base column schema (#5452) * Update 937d04c16b64_update_datasources.py (#7361) * Feature flag for client cache (#7348) * Feature flag for client cache * Fix integration test * Revert "Fix integration test" This reverts commit 58434ab98a015d6e96db4a97f26255aa282d989d. * Feature flag for client cache * Fix integration tests * Add feature flag to config.py * Add another feature check * Fix more integration tests * Fix raw HTML in SliceAdder (#7338) * remove backendSync.json (#7331) * [bubbles] issue when using duplicated metrics (#7087) * SUPERSET-7: Docker compose config version breaks on Ubuntu 16.04 (#7359) * SUPERSET-8: Update text in docs copyright footer (#7360) * SUPERSET-7: Docker compose config version breaks on Ubuntu 16.04 * SUPERSET-8: Extra text in docs copyright footer * [schema] Adding commits and removing unnecessary foreign-key definitions (#7371) * Store last selected dashboard in sessionStorage (#7181) * Store last selected dashboard in sessionStorage * Fix tests * [schema] Updating the base metric schema (#5453) * Fix NoneType bug & fill the test recipients with original recipients if empty (#7365) * feat: see Presto row and array data types (#7391) * feat: see Presto row and array data types * fix: address PR comments * fix: lint and build issues * fix: add types * add stronger type hints where possible * fix: lint issues and add select_star func in Hive * add missing pkg init * fix: build issues * fix: pylint issues * fix: use logging instead of print * Removed --console-log and superset runserver (#7421) * Fixes dashboard export button missing download and #7353 (#7427) * Added additional German translations to string file (#6604) * Added additional German translations to string file Updates to German translation files as per directions * Removed messages.json * [fix] Fixing SQL parsing issue (#7374) * add chinese translate (#7402) * Quick fix to address deadlock issue (#7434) * feat: view presto row objects in data grid (#7445) * Merge lastest from master into lyft-release-sp8 (#7405) * filter out all nan series (#7313) * improve not rich tooltip (#7345) * Create issue_label_bot.yaml (#7341) * fix: do not save colors without a color scheme (#7347) * [wtforms] Strip leading/trailing whitespace (#7084) * [schema] Updating the datasources schema (#5451) * limit tables/views returned if schema is not provided (#7358) * limit tables/views returned if schema is not provided * fix typo * improve code performance * handle the case when table name or view name does not present a schema * Add type anno (#7342) * Updated local dev instructions to include missing step * First pass at type annotations * [schema] Updating the base column schema (#5452) * Update 937d04c16b64_update_datasources.py (#7361) * Feature flag for client cache (#7348) * Feature flag for client cache * Fix integration test * Revert "Fix integration test" This reverts commit 58434ab98a015d6e96db4a97f26255aa282d989d. * Feature flag for client cache * Fix integration tests * Add feature flag to config.py * Add another feature check * Fix more integration tests * Fix raw HTML in SliceAdder (#7338) * remove backendSync.json (#7331) * [bubbles] issue when using duplicated metrics (#7087) * SUPERSET-7: Docker compose config version breaks on Ubuntu 16.04 (#7359) * SUPERSET-8: Update text in docs copyright footer (#7360) * SUPERSET-7: Docker compose config version breaks on Ubuntu 16.04 * SUPERSET-8: Extra text in docs copyright footer * [schema] Adding commits and removing unnecessary foreign-key definitions (#7371) * Store last selected dashboard in sessionStorage (#7181) * Store last selected dashboard in sessionStorage * Fix tests * [schema] Updating the base metric schema (#5453) * Fix NoneType bug & fill the test recipients with original recipients if empty (#7365) * feat: see Presto row and array data types (#7391) * feat: see Presto row and array data types * fix: address PR comments * fix: lint and build issues * fix: add types * Incorporate feedback from initial PR (prematurely merged to lyft-release-sp8) (#7415) * add stronger type hints where possible * fix: lint issues and add select_star func in Hive * add missing pkg init * fix: build issues * fix: pylint issues * fix: use logging instead of print * feat: view presto row objects in data grid * fix: address feedback * fix: spacing * feat: Scheduling queries from SQL Lab (#7416) (#7446) * Merge lastest from master into lyft-release-sp8 (#7405) * filter out all nan series (#7313) * improve not rich tooltip (#7345) * Create issue_label_bot.yaml (#7341) * fix: do not save colors without a color scheme (#7347) * [wtforms] Strip leading/trailing whitespace (#7084) * [schema] Updating the datasources schema (#5451) * limit tables/views returned if schema is not provided (#7358) * limit tables/views returned if schema is not provided * fix typo * improve code performance * handle the case when table name or view name does not present a schema * Add type anno (#7342) * Updated local dev instructions to include missing step * First pass at type annotations * [schema] Updating the base column schema (#5452) * Update 937d04c16b64_update_datasources.py (#7361) * Feature flag for client cache (#7348) * Feature flag for client cache * Fix integration test * Revert "Fix integration test" This reverts commit 58434ab98a015d6e96db4a97f26255aa282d989d. * Feature flag for client cache * Fix integration tests * Add feature flag to config.py * Add another feature check * Fix more integration tests * Fix raw HTML in SliceAdder (#7338) * remove backendSync.json (#7331) * [bubbles] issue when using duplicated metrics (#7087) * SUPERSET-7: Docker compose config version breaks on Ubuntu 16.04 (#7359) * SUPERSET-8: Update text in docs copyright footer (#7360) * SUPERSET-7: Docker compose config version breaks on Ubuntu 16.04 * SUPERSET-8: Extra text in docs copyright footer * [schema] Adding commits and removing unnecessary foreign-key definitions (#7371) * Store last selected dashboard in sessionStorage (#7181) * Store last selected dashboard in sessionStorage * Fix tests * [schema] Updating the base metric schema (#5453) * Fix NoneType bug & fill the test recipients with original recipients if empty (#7365) * feat: see Presto row and array data types (#7391) * feat: see Presto row and array data types * fix: address PR comments * fix: lint and build issues * fix: add types * Incorporate feedback from initial PR (prematurely merged to lyft-release-sp8) (#7415) * add stronger type hints where possible * fix: lint issues and add select_star func in Hive * add missing pkg init * fix: build issues * fix: pylint issues * fix: use logging instead of print * feat: view presto row objects in data grid * fix: address feedback * fix: spacing * Workaround for no results returned (#7442) * feat: view presto row objects in data grid (#7436) * feat: view presto row objects in data grid * fix: address feedback * fix: spacing * feat: Scheduling queries from SQL Lab (#7416) * Lightweight pipelines POC * Add docs * Minor fixes * Remove Lyft URL * Use enum * Minor fix * Fix unit tests * Mark props as required * feat: Add `validate_sql_json` endpoint for checking that a given sql query is valid for the chosen database (#7422) (#7462) merge from lyft-release-sp8 to master * Adds missing metric sum__SP_RUR_TOTL (#7452) * Late import for optional lib pyhive (#7471) * Late import for optional lib pyhive * fix * fix: calendar heatmap examples (#7375) Fixing a set of examples that trip on ValueError vs TypeError * bugfix: Improve support for special characters in schema and table names (#7297) * Bugfix to SQL Lab to support tables and schemas with characters that require quoting * Remove debugging prints * Add uri encoding to secondary tables call * Quote schema names for presto * Quote selected_schema on Snowflake, MySQL and Hive * Remove redundant parens * Add python unit tests * Add js unit test * Fix flake8 linting error * [dashboard] After update filter, trigger new queries when charts are visible (#7233) * trigger query when chart is visible * add integration test * fix: alter sql columns to long text #7463 (#7476) Merge lyft-release-sp8@7bfe7bc to master * Refactor ConsoleLog (#7428) * Revised Chinese translation (#7464) * add chinese translate * edit chinese translation * druid connector: avoid using 'dimensions' for scan queries (#7377) After the following PyDruid change (contained in version 0.5.2) the Superset Histogram charts rendered with Druid data are broken: druid-io/pydruid@0a59a70 Bump the pydruid requirements accordingly in setup.py Issue: apache#7368 --- CONTRIBUTING.md | 13 +- README.md | 1 + UPDATING.md | 8 +- docs/faq.rst | 4 +- docs/installation.rst | 17 +- requirements-dev.txt | 1 - requirements.txt | 17 +- setup.py | 5 +- superset/__init__.py | 29 +- .../integration/dashboard/dashboard.helper.js | 3 +- .../integration/dashboard/index.test.js | 2 + .../cypress/integration/dashboard/tabs.js | 157 ++ .../components/TableSelector_spec.jsx | 20 + .../dashboard/actions/dashboardLayout_spec.js | 67 +- .../dashboard/components/Dashboard_spec.jsx | 10 +- .../ControlPanelsContainer_spec.jsx | 3 +- .../components/ExploreViewContainer_spec.jsx | 32 +- .../javascripts/explore/controlUtils_spec.jsx | 164 ++ .../spec/javascripts/explore/store_spec.jsx | 66 + superset/assets/src/SqlLab/actions/sqlLab.js | 6 +- .../SqlLab/components/QueryAutoRefresh.jsx | 2 +- superset/assets/src/chart/Chart.jsx | 48 +- .../assets/src/components/TableSelector.jsx | 10 +- .../src/dashboard/actions/dashboardLayout.js | 29 +- .../src/dashboard/components/Dashboard.jsx | 19 +- .../dashboard/components/DashboardBuilder.jsx | 8 +- .../dashboard/components/DashboardGrid.jsx | 3 + .../components/gridComponents/Chart.jsx | 30 +- .../components/gridComponents/ChartHolder.jsx | 2 + .../components/gridComponents/Column.jsx | 2 + .../components/gridComponents/Row.jsx | 2 + .../components/gridComponents/Tab.jsx | 7 +- .../components/gridComponents/Tabs.jsx | 1 + .../src/dashboard/containers/Dashboard.jsx | 4 +- .../containers/DashboardComponent.jsx | 1 + .../components/ExploreViewContainer.jsx | 3 +- superset/assets/src/explore/controlUtils.js | 124 + superset/assets/src/explore/controls.jsx | 1 + .../src/explore/reducers/exploreReducer.js | 13 +- .../src/explore/reducers/getInitialState.js | 3 +- superset/assets/src/explore/store.js | 127 +- superset/assets/src/featureFlags.ts | 1 - .../assets/src/visualizations/deckgl/utils.js | 3 + superset/cli.py | 98 +- superset/connectors/druid/models.py | 3 +- superset/data/__init__.py | 1 + superset/data/tabbed_dashboard.py | 324 +++ superset/data/world_bank.py | 2 +- superset/db_engine_specs.py | 7 +- superset/models/annotations.py | 2 +- superset/sql_parse.py | 39 +- superset/sql_validators/presto_db.py | 2 +- .../templates/superset/export_dashboards.html | 13 +- .../translations/de/LC_MESSAGES/messages.json | 2419 +---------------- .../translations/de/LC_MESSAGES/messages.mo | Bin 63955 -> 63813 bytes .../translations/de/LC_MESSAGES/messages.po | 14 +- .../translations/zh/LC_MESSAGES/messages.mo | Bin 90011 -> 86446 bytes .../translations/zh/LC_MESSAGES/messages.po | 100 +- superset/utils/core.py | 20 +- superset/views/annotations.py | 32 +- superset/views/core.py | 23 +- superset/viz.py | 2 +- tests/security_tests.py | 2 + tests/sql_parse_tests.py | 9 + tests/utils_tests.py | 16 + 65 files changed, 1385 insertions(+), 2811 deletions(-) create mode 100644 superset/assets/cypress/integration/dashboard/tabs.js create mode 100644 superset/assets/spec/javascripts/explore/controlUtils_spec.jsx create mode 100644 superset/assets/spec/javascripts/explore/store_spec.jsx create mode 100644 superset/assets/src/explore/controlUtils.js create mode 100644 superset/data/tabbed_dashboard.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cdca9cbe242d1..e620e6afe8b4f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -328,10 +328,19 @@ FLASK_ENV=development flask run -p 8088 --with-threads --reload --debugger #### Logging to the browser console -This feature is only available on Python 3. When debugging your application, you can have the server logs sent directly to the browser console: +This feature is only available on Python 3. When debugging your application, you can have the server logs sent directly to the browser console using the [ConsoleLog](https://github.com/betodealmeida/consolelog) package. You need to mutate the app, by adding the following to your `config.py` or `superset_config.py`: + +```python +from console_log import ConsoleLog + +def FLASK_APP_MUTATOR(app): + app.wsgi_app = ConsoleLog(app.wsgi_app, app.logger) +``` + +Then make sure you run your WSGI server using the right worker type: ```bash -FLASK_ENV=development flask run -p 8088 --with-threads --reload --debugger --console-log +FLASK_ENV=development gunicorn superset:app -k "geventwebsocket.gunicorn.workers.GeventWebSocketWorker" -b 127.0.0.1:8088 --reload ``` You can log anything to the browser console, including objects: diff --git a/README.md b/README.md index 68a7570894399..6b13d4e1fb5e4 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,7 @@ the world know they are using Superset. Join our growing community! 1. [Konfío](http://konfio.mx) 1. [Kuaishou](https://www.kuaishou.com/) 1. [Lime](https://www.limebike.com/) + 1. [Living Goods](https://www.livinggoods.org) 1. [Lyft](https://www.lyft.com/) 1. [Maieutical Labs](https://maieuticallabs.it) 1. [Myra Labs](http://www.myralabs.com/) diff --git a/UPDATING.md b/UPDATING.md index f8d64ab48ec9a..59b558b2895f1 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -21,7 +21,7 @@ under the License. This file documents any backwards-incompatible changes in Superset and assists people when migrating to a new version. -## Superset 0.34.0 +## Next Version * [5451](https://github.com/apache/incubator-superset/pull/5451): a change which adds missing non-nullable fields to the `datasources` table. Depending on @@ -31,7 +31,11 @@ the integrity of the data, manual intervention may be required. which adds missing non-nullable fields and uniqueness constraints to the `columns`and `table_columns` tables. Depending on the integrity of the data, manual intervention may be required. - +* `fabmanager` command line is deprecated since Flask-AppBuilder 2.0.0, use +the new `flask fab ` integrated with *Flask cli*. +* `SUPERSET_UPDATE_PERMS` environment variable was replaced by +`FAB_UPDATE_PERMS` config boolean key. To disable automatic +creation of permissions set `FAB_UPDATE_PERMS = False` on config. * [5453](https://github.com/apache/incubator-superset/pull/5453): a change which adds missing non-nullable fields and uniqueness constraints to the metrics and sql_metrics tables. Depending on the integrity of the data, manual diff --git a/docs/faq.rst b/docs/faq.rst index d1f781f4a4172..426e0ab13b2d9 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -186,8 +186,8 @@ by setting the ``stagger_refresh`` to ``false`` and modify the stagger period by Here, the entire dashboard will refresh at once if periodic refresh is on. The stagger time of 2.5 seconds is ignored. -Why does fabmanager or superset freezed/hung/not responding when started (my home directory is NFS mounted)? ------------------------------------------------------------------------------------------------------------- +Why does 'flask fab' or superset freezed/hung/not responding when started (my home directory is NFS mounted)? +------------------------------------------------------------------------------------------------------------- By default, superset creates and uses an sqlite database at ``~/.superset/superset.db``. Sqlite is known to `don't work well if used on NFS`__ due to broken file locking implementation on NFS. __ https://www.sqlite.org/lockingv3.html diff --git a/docs/installation.rst b/docs/installation.rst index 72fd5b0b40d4e..c7c24fc1d4e67 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -170,12 +170,13 @@ Follow these few simple steps to install Superset.:: # Install superset pip install superset - # Create an admin user (you will be prompted to set a username, first and last name before setting a password) - fabmanager create-admin --app superset - # Initialize the database superset db upgrade + # Create an admin user (you will be prompted to set a username, first and last name before setting a password) + $ export FLASK_APP=superset + flask fab create-admin + # Load some data to play with superset load_examples @@ -183,7 +184,7 @@ Follow these few simple steps to install Superset.:: superset init # To start a development web server on port 8088, use -p to bind to another port - superset runserver -d + flask run -p 8080 --with-threads --reload --debugger After installation, you should be able to point your browser to the right @@ -236,17 +237,11 @@ workers this creates a lot of contention and race conditions when defining permissions and views. To alleviate this issue, the automatic updating of permissions can be disabled -by setting the environment variable -`SUPERSET_UPDATE_PERMS` environment variable to `0`. -The value `1` enables it, `0` disables it. Note if undefined the functionality -is enabled to maintain backwards compatibility. +by setting `FAB_UPDATE_PERMS = False` (defaults to True). In a production environment initialization could take on the following form: - export SUPERSET_UPDATE_PERMS=1 superset init - - export SUPERSET_UPDATE_PERMS=0 gunicorn -w 10 ... superset:app Configuration behind a load balancer diff --git a/requirements-dev.txt b/requirements-dev.txt index 1ef6617b5de87..857b9ad07d09f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -14,7 +14,6 @@ # See the License for the specific language governing permissions and # limitations under the License. # -console_log==0.2.10 coverage==4.5.3 flake8-commas==2.0.0 flake8-import-order==0.18 diff --git a/requirements.txt b/requirements.txt index bab2c19fdc177..1863a222b7c3f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,9 @@ # alembic==1.0.0 # via flask-migrate amqp==2.3.2 # via kombu +apispec[yaml]==1.2.0 # via flask-appbuilder asn1crypto==0.24.0 # via cryptography +attrs==19.1.0 # via jsonschema babel==2.6.0 # via flask-babel billiard==3.5.0.4 # via celery bleach==3.0.2 @@ -21,10 +23,11 @@ croniter==0.3.29 cryptography==2.4.2 decorator==4.3.0 # via retry defusedxml==0.5.0 # via python3-openid -flask-appbuilder==1.12.5 +flask-appbuilder==2.0.0 flask-babel==0.11.1 # via flask-appbuilder flask-caching==1.4.0 flask-compress==1.4.0 +flask-jwt-extended==3.18.1 # via flask-appbuilder flask-login==0.4.1 # via flask-appbuilder flask-migrate==2.1.1 flask-openid==1.2.5 # via flask-appbuilder @@ -38,19 +41,25 @@ idna==2.6 isodate==0.6.0 itsdangerous==0.24 # via flask jinja2==2.10 # via flask, flask-babel +jsonschema==3.0.1 # via flask-appbuilder kombu==4.2.1 # via celery mako==1.0.7 # via alembic markdown==3.0 markupsafe==1.0 # via jinja2, mako +marshmallow-enum==1.4.1 # via flask-appbuilder +marshmallow-sqlalchemy==0.16.2 # via flask-appbuilder +marshmallow==2.19.2 # via flask-appbuilder, marshmallow-enum, marshmallow-sqlalchemy numpy==1.15.2 # via pandas pandas==0.23.4 parsedatetime==2.0.0 pathlib2==2.3.0 polyline==1.3.2 +prison==0.1.0 # via flask-appbuilder py==1.7.0 # via retry pycparser==2.19 # via cffi pydruid==0.5.2 -pyjwt==1.7.1 # via flask-appbuilder +pyjwt==1.7.1 # via flask-appbuilder, flask-jwt-extended +pyrsistent==0.14.11 # via jsonschema python-dateutil==2.6.1 python-editor==1.0.3 # via alembic python-geohash==0.8.5 @@ -61,7 +70,7 @@ requests==2.20.0 retry==0.9.2 selenium==3.141.0 simplejson==3.15.0 -six==1.11.0 # via bleach, cryptography, isodate, pathlib2, polyline, pydruid, python-dateutil, sqlalchemy-utils, wtforms-json +six==1.11.0 # via bleach, cryptography, flask-jwt-extended, isodate, jsonschema, pathlib2, polyline, prison, pydruid, pyrsistent, python-dateutil, sqlalchemy-utils, wtforms-json sqlalchemy-utils==0.32.21 sqlalchemy==1.3.1 sqlparse==0.2.4 @@ -69,6 +78,6 @@ unicodecsv==0.14.1 urllib3==1.22 # via requests, selenium vine==1.1.4 # via amqp webencodings==0.5.1 # via bleach -werkzeug==0.14.1 # via flask +werkzeug==0.14.1 # via flask, flask-jwt-extended wtforms-json==0.3.3 wtforms==2.2.1 # via flask-wtf, wtforms-json diff --git a/setup.py b/setup.py index 9c6278e75ce74..2cf3e39b201de 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ def get_git_sha(): 'croniter>=0.3.28', 'cryptography>=2.4.2', 'flask>=1.0.0, <2.0.0', - 'flask-appbuilder>=1.12.5, <2.0.0', + 'flask-appbuilder>=2.0.0, <2.3.0', 'flask-caching', 'flask-compress', 'flask-migrate', @@ -92,7 +92,7 @@ def get_git_sha(): 'parsedatetime', 'pathlib2', 'polyline', - 'pydruid>=0.4.3', + 'pydruid>=0.5.2', 'python-dateutil', 'python-geohash', 'pyyaml>=3.13', @@ -108,7 +108,6 @@ def get_git_sha(): ], extras_require={ 'cors': ['flask-cors>=2.0.0'], - 'console_log': ['console_log==0.2.10'], 'hive': [ 'pyhive[hive]>=0.6.1', 'tableschema', diff --git a/superset/__init__.py b/superset/__init__.py index 7d0df26e039e7..8f8936ded38b2 100644 --- a/superset/__init__.py +++ b/superset/__init__.py @@ -34,8 +34,7 @@ from superset import config from superset.connectors.connector_registry import ConnectorRegistry from superset.security import SupersetSecurityManager -from superset.utils.core import ( - get_update_perms_flag, pessimistic_connection_handling, setup_cache) +from superset.utils.core import pessimistic_connection_handling, setup_cache wtforms_json.init() @@ -196,14 +195,14 @@ def index(self): not FAB's security manager. See [4565] in UPDATING.md""") -appbuilder = AppBuilder( - app, - db.session, - base_template='superset/base.html', - indexview=MyIndexView, - security_manager_class=custom_sm, - update_perms=get_update_perms_flag(), -) +with app.app_context(): + appbuilder = AppBuilder( + app, + db.session, + base_template='superset/base.html', + indexview=MyIndexView, + security_manager_class=custom_sm, + ) security_manager = appbuilder.sm @@ -226,11 +225,6 @@ def is_feature_enabled(feature): return get_feature_flags().get(feature) -# Registering sources -module_datasource_map = app.config.get('DEFAULT_MODULE_DS_MAP') -module_datasource_map.update(app.config.get('ADDITIONAL_MODULE_DS_MAP')) -ConnectorRegistry.register_sources(module_datasource_map) - # Flask-Compress if conf.get('ENABLE_FLASK_COMPRESS'): Compress(app) @@ -242,3 +236,8 @@ def is_feature_enabled(feature): flask_app_mutator(app) from superset import views # noqa + +# Registering sources +module_datasource_map = app.config.get('DEFAULT_MODULE_DS_MAP') +module_datasource_map.update(app.config.get('ADDITIONAL_MODULE_DS_MAP')) +ConnectorRegistry.register_sources(module_datasource_map) diff --git a/superset/assets/cypress/integration/dashboard/dashboard.helper.js b/superset/assets/cypress/integration/dashboard/dashboard.helper.js index 7e3788fddc899..1c16a82901298 100644 --- a/superset/assets/cypress/integration/dashboard/dashboard.helper.js +++ b/superset/assets/cypress/integration/dashboard/dashboard.helper.js @@ -16,7 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -export const WORLD_HEALTH_DASHBOARD = '/superset/dashboard/world_health'; +export const WORLD_HEALTH_DASHBOARD = '/superset/dashboard/world_health/'; +export const TABBED_DASHBOARD = '/superset/dashboard/tabbed_dash/'; export const CHECK_DASHBOARD_FAVORITE_ENDPOINT = '/superset/favstar/Dashboard/*/count'; diff --git a/superset/assets/cypress/integration/dashboard/index.test.js b/superset/assets/cypress/integration/dashboard/index.test.js index 9763b91737161..cc608e7d5b93b 100644 --- a/superset/assets/cypress/integration/dashboard/index.test.js +++ b/superset/assets/cypress/integration/dashboard/index.test.js @@ -22,6 +22,7 @@ import DashboardFavStarTest from './fav_star'; import DashboardFilterTest from './filter'; import DashboardLoadTest from './load'; import DashboardSaveTest from './save'; +import DashboardTabsTest from './tabs'; describe('Dashboard', () => { DashboardControlsTest(); @@ -30,4 +31,5 @@ describe('Dashboard', () => { DashboardFilterTest(); DashboardLoadTest(); DashboardSaveTest(); + DashboardTabsTest(); }); diff --git a/superset/assets/cypress/integration/dashboard/tabs.js b/superset/assets/cypress/integration/dashboard/tabs.js new file mode 100644 index 0000000000000..5029a00852dba --- /dev/null +++ b/superset/assets/cypress/integration/dashboard/tabs.js @@ -0,0 +1,157 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { TABBED_DASHBOARD } from './dashboard.helper'; + +export default () => describe('tabs', () => { + let filterId; + let treemapId; + let linechartId; + let boxplotId; + + // cypress can not handle window.scrollTo + // https://github.com/cypress-io/cypress/issues/2761 + // add this exception handler to pass test + const handleException = () => { + // return false to prevent the error from + // failing this test + cy.on('uncaught:exception', () => false); + }; + + beforeEach(() => { + cy.server(); + cy.login(); + + cy.visit(TABBED_DASHBOARD); + + cy.get('#app').then((data) => { + const bootstrapData = JSON.parse(data[0].dataset.bootstrap); + const dashboard = bootstrapData.dashboard_data; + filterId = dashboard.slices.find(slice => (slice.form_data.viz_type === 'filter_box')).slice_id; + boxplotId = dashboard.slices.find(slice => (slice.form_data.viz_type === 'box_plot')).slice_id; + treemapId = dashboard.slices.find(slice => (slice.form_data.viz_type === 'treemap')).slice_id; + linechartId = dashboard.slices.find(slice => (slice.form_data.viz_type === 'line')).slice_id; + + const filterFormdata = { + slice_id: filterId, + }; + const filterRequest = `/superset/explore_json/?form_data=${JSON.stringify(filterFormdata)}`; + cy.route('POST', filterRequest).as('filterRequest'); + + const treemapFormdata = { + slice_id: treemapId, + }; + const treemapRequest = `/superset/explore_json/?form_data=${JSON.stringify(treemapFormdata)}`; + cy.route('POST', treemapRequest).as('treemapRequest'); + + const linechartFormdata = { + slice_id: linechartId, + }; + const linechartRequest = `/superset/explore_json/?form_data=${JSON.stringify(linechartFormdata)}`; + cy.route('POST', linechartRequest).as('linechartRequest'); + + const boxplotFormdata = { + slice_id: boxplotId, + }; + const boxplotRequest = `/superset/explore_json/?form_data=${JSON.stringify(boxplotFormdata)}`; + cy.route('POST', boxplotRequest).as('boxplotRequest'); + }); + }); + + it('should load charts when tab is visible', () => { + // landing in first tab, should see 2 charts + cy.wait('@filterRequest'); + cy.get('.grid-container .filter_box').should('be.exist'); + cy.wait('@treemapRequest'); + cy.get('.grid-container .treemap').should('be.exist'); + cy.get('.grid-container .box_plot').should('not.be.exist'); + cy.get('.grid-container .line').should('not.be.exist'); + + // click row level tab, see 1 more chart + cy.get('.tab-content ul.nav.nav-tabs li') + .last() + .find('.editable-title input') + .click(); + cy.wait('@linechartRequest'); + cy.get('.grid-container .line').should('be.exist'); + + // click top level tab, see 1 more chart + handleException(); + cy.get('.dashboard-component-tabs') + .first() + .find('ul.nav.nav-tabs li') + .last() + .find('.editable-title input') + .click(); + cy.wait('@boxplotRequest'); + cy.get('.grid-container .box_plot').should('be.exist'); + }); + + it('should send new queries when tab becomes visible', () => { + // landing in first tab + cy.wait('@filterRequest'); + cy.wait('@treemapRequest'); + + // creating route and stubbing filtered route + cy.route('POST', '/superset/explore_json/*').as('updatedChartRequest'); + + // apply filter + cy.get('.Select-control') + .first() + .find('input') + .first() + .type('South Asia{enter}', { force: true }); + + // send new query from same tab + cy.wait('@updatedChartRequest') + .then((xhr) => { + const requestFormData = xhr.request.body; + const requestParams = JSON.parse(requestFormData.get('form_data')); + expect(requestParams.extra_filters[0]) + .deep.eq({ col: 'region', op: 'in', val: ['South Asia'] }); + }); + + // click row level tab, send 1 more query + cy.get('.tab-content ul.nav.nav-tabs li') + .last() + .click(); + cy.wait('@updatedChartRequest') + .then((xhr) => { + const requestFormData = xhr.request.body; + const requestParams = JSON.parse(requestFormData.get('form_data')); + expect(requestParams.extra_filters[0]) + .deep.eq({ col: 'region', op: 'in', val: ['South Asia'] }); + }); + + // click top level tab, send 1 more query + handleException(); + cy.get('.dashboard-component-tabs') + .first() + .find('ul.nav.nav-tabs li') + .last() + .find('.editable-title input') + .click(); + cy.wait('@updatedChartRequest') + .then((xhr) => { + const requestFormData = xhr.request.body; + const requestParams = JSON.parse(requestFormData.get('form_data')); + expect(requestParams.extra_filters[0]) + .deep.eq({ col: 'region', op: 'in', val: ['South Asia'] }); + }); + }); +}); diff --git a/superset/assets/spec/javascripts/components/TableSelector_spec.jsx b/superset/assets/spec/javascripts/components/TableSelector_spec.jsx index 46d57631dacae..70e2cca1f1925 100644 --- a/superset/assets/spec/javascripts/components/TableSelector_spec.jsx +++ b/superset/assets/spec/javascripts/components/TableSelector_spec.jsx @@ -85,6 +85,7 @@ describe('TableSelector', () => { .getTableNamesBySubStr('') .then((data) => { expect(data).toEqual({ options: [] }); + return Promise.resolve(); })); it('should handle table name', () => { @@ -104,6 +105,23 @@ describe('TableSelector', () => { .then((data) => { expect(fetchMock.calls(GET_TABLE_NAMES_GLOB)).toHaveLength(1); expect(data).toEqual(mockTableOptions); + return Promise.resolve(); + }); + }); + + it('should escape schema and table names', () => { + const GET_TABLE_GLOB = 'glob:*/superset/tables/1/*/*'; + const mockTableOptions = { options: [table] }; + wrapper.setProps({ schema: 'slashed/schema' }); + fetchMock.get(GET_TABLE_GLOB, mockTableOptions, { overwriteRoutes: true }); + + return wrapper + .instance() + .getTableNamesBySubStr('slashed/table') + .then(() => { + expect(fetchMock.lastUrl(GET_TABLE_GLOB)) + .toContain('/slashed%252Fschema/slashed%252Ftable'); + return Promise.resolve(); }); }); }); @@ -125,6 +143,7 @@ describe('TableSelector', () => { .fetchTables(true, 'birth_names') .then(() => { expect(wrapper.state().tableOptions).toHaveLength(3); + return Promise.resolve(); }); }); @@ -138,6 +157,7 @@ describe('TableSelector', () => { expect(wrapper.state().tableOptions).toEqual([]); expect(wrapper.state().tableOptions).toHaveLength(0); expect(mockedProps.handleError.callCount).toBe(1); + return Promise.resolve(); }); }); }); diff --git a/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js b/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js index d57207d090c22..0dfca9cdbfa33 100644 --- a/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js +++ b/superset/assets/spec/javascripts/dashboard/actions/dashboardLayout_spec.js @@ -39,7 +39,10 @@ import { } from '../../../../src/dashboard/actions/dashboardLayout'; import { setUnsavedChanges } from '../../../../src/dashboard/actions/dashboardState'; -import { addInfoToast } from '../../../../src/messageToasts/actions'; +import { + addWarningToast, + ADD_TOAST, +} from '../../../../src/messageToasts/actions'; import { DASHBOARD_GRID_TYPE, @@ -334,7 +337,9 @@ describe('dashboardLayout actions', () => { const thunk = handleComponentDrop(dropResult); thunk(dispatch, getState); - expect(dispatch.getCall(0).args[0].type).toEqual(addInfoToast('').type); + expect(dispatch.getCall(0).args[0].type).toEqual( + addWarningToast('').type, + ); expect(dispatch.callCount).toBe(1); }); @@ -402,6 +407,64 @@ describe('dashboardLayout actions', () => { expect(dispatch.callCount).toBe(2); }); + + it('should dispatch a toast if drop top-level tab into nested tab', () => { + const { getState, dispatch } = setup({ + dashboardLayout: { + present: { + [DASHBOARD_ROOT_ID]: { + children: ['TABS-ROOT_TABS'], + id: DASHBOARD_ROOT_ID, + type: 'ROOT', + }, + 'TABS-ROOT_TABS': { + children: ['TAB-iMppmTOQy', 'TAB-rt1y8cQ6K9', 'TAB-X_pnCIwPN'], + id: 'TABS-ROOT_TABS', + meta: {}, + parents: ['ROOT_ID'], + type: TABS_TYPE, + }, + 'TABS-ROW_TABS': { + children: [ + 'TAB-dKIDBT03bQ', + 'TAB-PtxY5bbTe', + 'TAB-Wc2P-yGMz', + 'TAB-U-xe_si7i', + ], + id: 'TABS-ROW_TABS', + meta: {}, + parents: ['ROOT_ID', 'TABS-ROOT_TABS', 'TAB-X_pnCIwPN'], + type: TABS_TYPE, + }, + }, + }, + }); + const dropResult = { + source: { + id: 'TABS-ROOT_TABS', + index: 1, + type: TABS_TYPE, + }, + destination: { + id: 'TABS-ROW_TABS', + index: 1, + type: TABS_TYPE, + }, + dragging: { + id: 'TAB-rt1y8cQ6K9', + meta: { text: 'New Tab' }, + type: 'TAB', + }, + }; + + const thunk1 = handleComponentDrop(dropResult); + thunk1(dispatch, getState); + + const thunk2 = dispatch.getCall(0).args[0]; + thunk2(dispatch, getState); + + expect(dispatch.getCall(1).args[0].type).toEqual(ADD_TOAST); + }); }); describe('undoLayoutAction', () => { diff --git a/superset/assets/spec/javascripts/dashboard/components/Dashboard_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/Dashboard_spec.jsx index de637cd2ada9a..05d3675a55152 100644 --- a/superset/assets/spec/javascripts/dashboard/components/Dashboard_spec.jsx +++ b/superset/assets/spec/javascripts/dashboard/components/Dashboard_spec.jsx @@ -39,7 +39,7 @@ describe('Dashboard', () => { actions: { addSliceToDashboard() {}, removeSliceFromDashboard() {}, - postChartFormData() {}, + triggerQuery() {}, logEvent() {}, }, initMessages: [], @@ -82,15 +82,15 @@ describe('Dashboard', () => { }, }; - it('should call postChartFormData for all non-exempt slices', () => { + it('should call triggerQuery for all non-exempt slices', () => { const wrapper = setup({ charts: overrideCharts, slices: overrideSlices }); - const spy = sinon.spy(props.actions, 'postChartFormData'); + const spy = sinon.spy(props.actions, 'triggerQuery'); wrapper.instance().refreshExcept('1001'); spy.restore(); expect(spy.callCount).toBe(Object.keys(overrideCharts).length - 1); }); - it('should not call postChartFormData for filter_immune_slices', () => { + it('should not call triggerQuery for filter_immune_slices', () => { const wrapper = setup({ charts: overrideCharts, dashboardInfo: { @@ -103,7 +103,7 @@ describe('Dashboard', () => { }, }, }); - const spy = sinon.spy(props.actions, 'postChartFormData'); + const spy = sinon.spy(props.actions, 'triggerQuery'); wrapper.instance().refreshExcept(); spy.restore(); expect(spy.callCount).toBe(0); diff --git a/superset/assets/spec/javascripts/explore/components/ControlPanelsContainer_spec.jsx b/superset/assets/spec/javascripts/explore/components/ControlPanelsContainer_spec.jsx index 842c5e2640199..cda5e0ac67661 100644 --- a/superset/assets/spec/javascripts/explore/components/ControlPanelsContainer_spec.jsx +++ b/superset/assets/spec/javascripts/explore/components/ControlPanelsContainer_spec.jsx @@ -18,7 +18,8 @@ */ import React from 'react'; import { shallow } from 'enzyme'; -import { getFormDataFromControls, defaultControls } from 'src/explore/store'; +import { defaultControls } from 'src/explore/store'; +import { getFormDataFromControls } from 'src/explore/controlUtils'; import { ControlPanelsContainer } from 'src/explore/components/ControlPanelsContainer'; import ControlPanelSection from 'src/explore/components/ControlPanelSection'; import * as featureFlags from 'src/featureFlags'; diff --git a/superset/assets/spec/javascripts/explore/components/ExploreViewContainer_spec.jsx b/superset/assets/spec/javascripts/explore/components/ExploreViewContainer_spec.jsx index ee3a006a75066..bdaa6420f6412 100644 --- a/superset/assets/spec/javascripts/explore/components/ExploreViewContainer_spec.jsx +++ b/superset/assets/spec/javascripts/explore/components/ExploreViewContainer_spec.jsx @@ -19,6 +19,7 @@ import React from 'react'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; +import sinon from 'sinon'; import { shallow } from 'enzyme'; import getInitialState from 'src/explore/reducers/getInitialState'; @@ -58,7 +59,7 @@ describe('ExploreViewContainer', () => { wrapper = shallow(, { context: { store }, disableLifecycleMethods: true, - }); + }).dive(); }); it('renders', () => { @@ -68,14 +69,37 @@ describe('ExploreViewContainer', () => { }); it('renders QueryAndSaveButtons', () => { - expect(wrapper.dive().find(QueryAndSaveBtns)).toHaveLength(1); + expect(wrapper.find(QueryAndSaveBtns)).toHaveLength(1); }); it('renders ControlPanelsContainer', () => { - expect(wrapper.dive().find(ControlPanelsContainer)).toHaveLength(1); + expect(wrapper.find(ControlPanelsContainer)).toHaveLength(1); }); it('renders ChartContainer', () => { - expect(wrapper.dive().find(ChartContainer)).toHaveLength(1); + expect(wrapper.find(ChartContainer)).toHaveLength(1); + }); + + describe('componentWillReceiveProps()', () => { + it('when controls change, should call resetControls', () => { + expect(wrapper.instance().props.controls.viz_type.value).toBe('table'); + const resetControls = sinon.stub(wrapper.instance().props.actions, 'resetControls'); + const triggerQuery = sinon.stub(wrapper.instance().props.actions, 'triggerQuery'); + + // triggers componentWillReceiveProps + wrapper.setProps({ + controls: { + viz_type: { + value: 'bar', + }, + }, + }); + expect(resetControls.callCount).toBe(1); + // exploreview container should not force chart run query + // it should be controlled by redux state. + expect(triggerQuery.callCount).toBe(0); + resetControls.reset(); + triggerQuery.reset(); + }); }); }); diff --git a/superset/assets/spec/javascripts/explore/controlUtils_spec.jsx b/superset/assets/spec/javascripts/explore/controlUtils_spec.jsx new file mode 100644 index 0000000000000..50f766a1b7b48 --- /dev/null +++ b/superset/assets/spec/javascripts/explore/controlUtils_spec.jsx @@ -0,0 +1,164 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { + getControlConfig, + getControlState, + getControlKeys, + applyMapStateToPropsToControl, +} from '../../../src/explore/controlUtils'; + +describe('controlUtils', () => { + const state = { + datasource: { + columns: [ + 'a', 'b', 'c', + ], + metrics: [ + { metric_name: 'first' }, + { metric_name: 'second' }, + ], + }, + }; + + describe('getControlConfig', () => { + it('returns a valid spatial controlConfig', () => { + const spatialControl = getControlConfig('spatial', 'deck_grid'); + expect(spatialControl.type).toEqual('SpatialControl'); + expect(spatialControl.validators).toHaveLength(1); + }); + it('overrides according to vizType', () => { + let control = getControlConfig('metric', 'line'); + expect(control.type).toEqual('MetricsControl'); + expect(control.validators).toHaveLength(1); + + // deck_polygon overrides and removes validators + control = getControlConfig('metric', 'deck_polygon'); + expect(control.type).toEqual('MetricsControl'); + expect(control.validators).toHaveLength(0); + }); + }); + + describe('getControlKeys', () => { + + window.featureFlags = { + SCOPED_FILTER: false, + }; + + it('gets only strings, even when React components are in conf', () => { + const keys = getControlKeys('filter_box'); + expect(keys.every(k => typeof k === 'string')).toEqual(true); + expect(keys).toHaveLength(16); + }); + it('gets the right set of controlKeys for filter_box', () => { + const keys = getControlKeys('filter_box'); + expect(keys.sort()).toEqual([ + 'adhoc_filters', + 'cache_timeout', + 'datasource', + 'date_filter', + 'druid_time_origin', + 'filter_configs', + 'granularity', + 'instant_filtering', + 'show_druid_time_granularity', + 'show_druid_time_origin', + 'show_sqla_time_column', + 'show_sqla_time_granularity', + 'slice_id', + 'time_range', + 'url_params', + 'viz_type', + ]); + }); + }); + + describe('applyMapStateToPropsToControl,', () => { + it('applies state to props as expected', () => { + let control = getControlConfig('all_columns', 'table'); + control = applyMapStateToPropsToControl(control, state); + expect(control.options).toEqual(['a', 'b', 'c']); + }); + + it('removes the mapStateToProps key from the object', () => { + let control = getControlConfig('all_columns', 'table'); + control = applyMapStateToPropsToControl(control, state); + expect(control.mapStateToProps).toBe(undefined); + }); + + }); + + describe('getControlState', () => { + + it('to be function free', () => { + const control = getControlState('all_columns', 'table', state, ['a']); + expect(control.mapStateToProps).toBe(undefined); + expect(control.validators).toBe(undefined); + }); + + it('to fix multi with non-array values', () => { + const control = getControlState('all_columns', 'table', state, 'a'); + expect(control.value).toEqual(['a']); + }); + + it('removes missing/invalid choice', () => { + let control = getControlState('stacked_style', 'area', state, 'stack'); + expect(control.value).toBe('stack'); + + control = getControlState('stacked_style', 'area', state, 'FOO'); + expect(control.value).toBe(null); + }); + + it('applies the default function for metrics', () => { + const control = getControlState('metrics', 'table', state); + expect(control.default).toEqual(['first']); + }); + + it('applies the default function for metric', () => { + const control = getControlState('metric', 'table', state); + expect(control.default).toEqual('first'); + }); + + it('applies the default function, prefers count if it exists', () => { + const stateWithCount = { + ...state, + datasource: { + ...state.datasource, + metrics: [ + { metric_name: 'first' }, + { metric_name: 'second' }, + { metric_name: 'count' }, + ], + }, + }; + const control = getControlState('metrics', 'table', stateWithCount); + expect(control.default).toEqual(['count']); + }); + + }); + + describe('validateControl', () => { + + it('validates the control, returns an error if empty', () => { + const control = getControlState('metric', 'table', state, null); + expect(control.validationErrors).toEqual(['cannot be empty']); + }); + + }); + +}); diff --git a/superset/assets/spec/javascripts/explore/store_spec.jsx b/superset/assets/spec/javascripts/explore/store_spec.jsx new file mode 100644 index 0000000000000..4884ed180f1ed --- /dev/null +++ b/superset/assets/spec/javascripts/explore/store_spec.jsx @@ -0,0 +1,66 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { applyDefaultFormData } from '../../../src/explore/store'; + +describe('store', () => { + + describe('applyDefaultFormData', () => { + + window.featureFlags = { + SCOPED_FILTER: false, + }; + + it('applies default to formData if the key is missing', () => { + const inputFormData = { + datasource: '11_table', + viz_type: 'table', + }; + let outputFormData = applyDefaultFormData(inputFormData); + expect(outputFormData.row_limit).toEqual(10000); + + const inputWithRowLimit = { + ...inputFormData, + row_limit: 888, + }; + outputFormData = applyDefaultFormData(inputWithRowLimit); + expect(outputFormData.row_limit).toEqual(888); + }); + + it('keeps null if key is defined with null', () => { + const inputFormData = { + datasource: '11_table', + viz_type: 'table', + row_limit: null, + }; + const outputFormData = applyDefaultFormData(inputFormData); + expect(outputFormData.row_limit).toBe(null); + }); + + it('removes out of scope, or deprecated keys', () => { + const inputFormData = { + datasource: '11_table', + viz_type: 'table', + this_should_no_be_here: true, + }; + const outputFormData = applyDefaultFormData(inputFormData); + expect(outputFormData.this_should_no_be_here).toBe(undefined); + }); + + }); +}); diff --git a/superset/assets/src/SqlLab/actions/sqlLab.js b/superset/assets/src/SqlLab/actions/sqlLab.js index b8e785a574bc5..81c8e8d5593ec 100644 --- a/superset/assets/src/SqlLab/actions/sqlLab.js +++ b/superset/assets/src/SqlLab/actions/sqlLab.js @@ -339,7 +339,8 @@ export function addTable(query, tableName, schemaName) { }), ); - SupersetClient.get({ endpoint: `/superset/table/${query.dbId}/${tableName}/${schemaName}/` }) + SupersetClient.get({ endpoint: encodeURI(`/superset/table/${query.dbId}/` + + `${encodeURIComponent(tableName)}/${encodeURIComponent(schemaName)}/`) }) .then(({ json }) => { const dataPreviewQuery = { id: shortid.generate(), @@ -376,7 +377,8 @@ export function addTable(query, tableName, schemaName) { ); SupersetClient.get({ - endpoint: `/superset/extra_table_metadata/${query.dbId}/${tableName}/${schemaName}/`, + endpoint: encodeURI(`/superset/extra_table_metadata/${query.dbId}/` + + `${encodeURIComponent(tableName)}/${encodeURIComponent(schemaName)}/`), }) .then(({ json }) => dispatch(mergeTable({ ...table, ...json, isExtraMetadataLoading: false })), diff --git a/superset/assets/src/SqlLab/components/QueryAutoRefresh.jsx b/superset/assets/src/SqlLab/components/QueryAutoRefresh.jsx index 13834cb9d919a..8ad02adad3bf9 100644 --- a/superset/assets/src/SqlLab/components/QueryAutoRefresh.jsx +++ b/superset/assets/src/SqlLab/components/QueryAutoRefresh.jsx @@ -27,7 +27,7 @@ import * as Actions from '../actions/sqlLab'; const QUERY_UPDATE_FREQ = 2000; const QUERY_UPDATE_BUFFER_MS = 5000; const MAX_QUERY_AGE_TO_POLL = 21600000; -const QUERY_TIMEOUT_LIMIT = 7000; +const QUERY_TIMEOUT_LIMIT = 10000; class QueryAutoRefresh extends React.PureComponent { componentWillMount() { diff --git a/superset/assets/src/chart/Chart.jsx b/superset/assets/src/chart/Chart.jsx index cfc6ce8a97053..0218a17a4f8f5 100644 --- a/superset/assets/src/chart/Chart.jsx +++ b/superset/assets/src/chart/Chart.jsx @@ -22,6 +22,7 @@ import { Alert } from 'react-bootstrap'; import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags'; import { Logger, LOG_ACTIONS_RENDER_CHART_CONTAINER } from '../logger/LogUtils'; +import { safeStringify } from '../utils/safeStringify'; import Loading from '../components/Loading'; import RefreshChartOverlay from '../components/RefreshChartOverlay'; import StackTraceMessage from '../components/StackTraceMessage'; @@ -69,25 +70,38 @@ class Chart extends React.PureComponent { super(props); this.handleRenderContainerFailure = this.handleRenderContainerFailure.bind(this); } + componentDidMount() { if (this.props.triggerQuery) { - if (this.props.chartId > 0 && isFeatureEnabled(FeatureFlag.CLIENT_CACHE)) { - // Load saved chart with a GET request - this.props.actions.getSavedChart( - this.props.formData, - false, - this.props.timeout, - this.props.chartId, - ); - } else { - // Create chart with POST request - this.props.actions.postChartFormData( - this.props.formData, - false, - this.props.timeout, - this.props.chartId, - ); - } + this.runQuery(); + } + } + + componentDidUpdate(prevProps) { + if (this.props.triggerQuery && + safeStringify(prevProps.formData) !== safeStringify(this.props.formData) + ) { + this.runQuery(); + } + } + + runQuery() { + if (this.props.chartId > 0 && isFeatureEnabled(FeatureFlag.CLIENT_CACHE)) { + // Load saved chart with a GET request + this.props.actions.getSavedChart( + this.props.formData, + false, + this.props.timeout, + this.props.chartId, + ); + } else { + // Create chart with POST request + this.props.actions.postChartFormData( + this.props.formData, + false, + this.props.timeout, + this.props.chartId, + ); } } diff --git a/superset/assets/src/components/TableSelector.jsx b/superset/assets/src/components/TableSelector.jsx index c3e405ed48313..ba2cebb2799d8 100644 --- a/superset/assets/src/components/TableSelector.jsx +++ b/superset/assets/src/components/TableSelector.jsx @@ -90,7 +90,7 @@ export default class TableSelector extends React.PureComponent { onChange() { this.props.onChange({ dbId: this.state.dbId, - shema: this.state.schema, + schema: this.state.schema, tableName: this.state.tableName, }); } @@ -101,9 +101,8 @@ export default class TableSelector extends React.PureComponent { return Promise.resolve({ options }); } return SupersetClient.get({ - endpoint: ( - `/superset/tables/${this.props.dbId}/` + - `${this.props.schema}/${input}`), + endpoint: encodeURI(`/superset/tables/${this.props.dbId}/` + + `${encodeURIComponent(this.props.schema)}/${encodeURIComponent(input)}`), }).then(({ json }) => ({ options: this.addOptionIfMissing(json.options, tableName) })); } dbMutator(data) { @@ -123,7 +122,8 @@ export default class TableSelector extends React.PureComponent { const { dbId, schema } = this.props; if (dbId && schema) { this.setState(() => ({ tableLoading: true, tableOptions: [] })); - const endpoint = `/superset/tables/${dbId}/${schema}/${substr}/${forceRefresh}/`; + const endpoint = encodeURI(`/superset/tables/${dbId}/` + + `${encodeURIComponent(schema)}/${encodeURIComponent(substr)}/${forceRefresh}/`); return SupersetClient.get({ endpoint }) .then(({ json }) => { const filterOptions = createFilterOptions({ options: json.options }); diff --git a/superset/assets/src/dashboard/actions/dashboardLayout.js b/superset/assets/src/dashboard/actions/dashboardLayout.js index b76966c934c47..b276e374bb909 100644 --- a/superset/assets/src/dashboard/actions/dashboardLayout.js +++ b/superset/assets/src/dashboard/actions/dashboardLayout.js @@ -17,8 +17,9 @@ * under the License. */ import { ActionCreators as UndoActionCreators } from 'redux-undo'; +import { t } from '@superset-ui/translation'; -import { addInfoToast } from '../../messageToasts/actions'; +import { addWarningToast } from '../../messageToasts/actions'; import { setUnsavedChanges } from './dashboardState'; import { TABS_TYPE, ROW_TYPE } from '../util/componentTypes'; import { @@ -153,8 +154,10 @@ export function handleComponentDrop(dropResult) { if (overflowsParent) { return dispatch( - addInfoToast( - `There is not enough space for this component. Try decreasing its width, or increasing the destination width.`, + addWarningToast( + t( + `There is not enough space for this component. Try decreasing its width, or increasing the destination width.`, + ), ), ); } @@ -162,11 +165,29 @@ export function handleComponentDrop(dropResult) { const { source, destination } = dropResult; const droppedOnRoot = destination && destination.id === DASHBOARD_ROOT_ID; const isNewComponent = source.id === NEW_COMPONENTS_SOURCE_ID; + const dashboardRoot = getState().dashboardLayout.present[DASHBOARD_ROOT_ID]; + const rootChildId = + dashboardRoot && dashboardRoot.children ? dashboardRoot.children[0] : ''; if (droppedOnRoot) { dispatch(createTopLevelTabs(dropResult)); } else if (destination && isNewComponent) { dispatch(createComponent(dropResult)); + } else if ( + // Add additional allow-to-drop logic for tag/tags source. + // We only allow + // - top-level tab => top-level tab: rearrange top-level tab order + // - nested tab => top-level tab: allow row tab become top-level tab + // Dashboard does not allow top-level tab become nested tab, to avoid + // nested tab inside nested tab. + source.type === TABS_TYPE && + destination.type === TABS_TYPE && + source.id === rootChildId && + destination.id !== rootChildId + ) { + return dispatch( + addWarningToast(t(`Can not move top level tab into nested tabs`)), + ); } else if ( destination && source && @@ -176,6 +197,8 @@ export function handleComponentDrop(dropResult) { dispatch(moveComponent(dropResult)); } + // call getState() again down here in case redux state is stale after + // previous dispatch(es) const { dashboardLayout: undoableLayout } = getState(); // if we moved a child from a Tab or Row parent and it was the only child, delete the parent. diff --git a/superset/assets/src/dashboard/components/Dashboard.jsx b/superset/assets/src/dashboard/components/Dashboard.jsx index b26cde6f21183..b5845597a9a75 100644 --- a/superset/assets/src/dashboard/components/Dashboard.jsx +++ b/superset/assets/src/dashboard/components/Dashboard.jsx @@ -30,7 +30,6 @@ import { loadStatsPropShape, } from '../util/propShapes'; import { areObjectsEqual } from '../../reduxUtils'; -import getFormDataWithExtraFilters from '../util/charts/getFormDataWithExtraFilters'; import { LOG_ACTIONS_MOUNT_DASHBOARD } from '../../logger/LogUtils'; import OmniContainer from '../../components/OmniContainer'; @@ -40,7 +39,7 @@ const propTypes = { actions: PropTypes.shape({ addSliceToDashboard: PropTypes.func.isRequired, removeSliceFromDashboard: PropTypes.func.isRequired, - postChartFormData: PropTypes.func.isRequired, + triggerQuery: PropTypes.func.isRequired, logEvent: PropTypes.func.isRequired, }).isRequired, dashboardInfo: dashboardInfoPropShape.isRequired, @@ -149,21 +148,7 @@ class Dashboard extends React.PureComponent { this.getAllCharts().forEach(chart => { // filterKey is a string, immune array contains numbers if (String(chart.id) !== filterKey && immune.indexOf(chart.id) === -1) { - const updatedFormData = getFormDataWithExtraFilters({ - chart, - dashboardMetadata: this.props.dashboardInfo.metadata, - filters: this.props.dashboardState.filters, - colorScheme: this.props.dashboardState.colorScheme, - colorNamespace: this.props.dashboardState.colorNamespace, - sliceId: chart.id, - }); - - this.props.actions.postChartFormData( - updatedFormData, - false, - this.props.timeout, - chart.id, - ); + this.props.actions.triggerQuery(true, chart.id); } }); } diff --git a/superset/assets/src/dashboard/components/DashboardBuilder.jsx b/superset/assets/src/dashboard/components/DashboardBuilder.jsx index 12c8ff3368cbd..eadaab4e80502 100644 --- a/superset/assets/src/dashboard/components/DashboardBuilder.jsx +++ b/superset/assets/src/dashboard/components/DashboardBuilder.jsx @@ -84,10 +84,11 @@ class DashboardBuilder extends React.Component { const { dashboardLayout, directPathToChild } = props; const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID]; const rootChildId = dashboardRoot.children[0]; - const topLevelTabs = - rootChildId !== DASHBOARD_GRID_ID && dashboardLayout[rootChildId]; const tabIndex = findTabIndexByComponentId({ - currentComponent: topLevelTabs || dashboardLayout[DASHBOARD_ROOT_ID], + currentComponent: + rootChildId === DASHBOARD_GRID_ID + ? dashboardLayout[DASHBOARD_ROOT_ID] + : dashboardLayout[rootChildId], directPathToChild, }); @@ -221,6 +222,7 @@ class DashboardBuilder extends React.Component { // see isValidChild for why tabs do not increment the depth of their children depth={DASHBOARD_ROOT_DEPTH + 1} // (topLevelTabs ? 0 : 1)} width={width} + isComponentVisible={index === tabIndex} /> ))} diff --git a/superset/assets/src/dashboard/components/DashboardGrid.jsx b/superset/assets/src/dashboard/components/DashboardGrid.jsx index b036ad006c64b..0666f477380a5 100644 --- a/superset/assets/src/dashboard/components/DashboardGrid.jsx +++ b/superset/assets/src/dashboard/components/DashboardGrid.jsx @@ -30,6 +30,7 @@ const propTypes = { editMode: PropTypes.bool.isRequired, gridComponent: componentShape.isRequired, handleComponentDrop: PropTypes.func.isRequired, + isComponentVisible: PropTypes.bool.isRequired, resizeComponent: PropTypes.func.isRequired, width: PropTypes.number.isRequired, }; @@ -114,6 +115,7 @@ class DashboardGrid extends React.PureComponent { depth, editMode, width, + isComponentVisible, } = this.props; const columnPlusGutterWidth = @@ -154,6 +156,7 @@ class DashboardGrid extends React.PureComponent { index={index} availableColumnCount={GRID_COLUMN_COUNT} columnWidth={columnWidth} + isComponentVisible={isComponentVisible} onResizeStart={this.handleResizeStart} onResize={this.handleResize} onResizeStop={this.handleResizeStop} diff --git a/superset/assets/src/dashboard/components/gridComponents/Chart.jsx b/superset/assets/src/dashboard/components/gridComponents/Chart.jsx index ff1120835534e..9b1b5f1637b76 100644 --- a/superset/assets/src/dashboard/components/gridComponents/Chart.jsx +++ b/superset/assets/src/dashboard/components/gridComponents/Chart.jsx @@ -37,6 +37,7 @@ const propTypes = { width: PropTypes.number.isRequired, height: PropTypes.number.isRequired, updateSliceName: PropTypes.func.isRequired, + isComponentVisible: PropTypes.bool, // from redux chart: PropTypes.shape(chartPropShape).isRequired, @@ -61,6 +62,7 @@ const propTypes = { const defaultProps = { isCached: false, + isComponentVisible: true, }; // we use state + shouldComponentUpdate() logic to prevent perf-wrecking @@ -99,19 +101,27 @@ class Chart extends React.Component { return true; } - for (let i = 0; i < SHOULD_UPDATE_ON_PROP_CHANGES.length; i += 1) { - const prop = SHOULD_UPDATE_ON_PROP_CHANGES[i]; - if (nextProps[prop] !== this.props[prop]) { + // allow chart update/re-render only if visible: + // under selected tab or no tab layout + if (nextProps.isComponentVisible) { + if (nextProps.chart.triggerQuery) { return true; } - } - if ( - nextProps.width !== this.props.width || - nextProps.height !== this.props.height - ) { - clearTimeout(this.resizeTimeout); - this.resizeTimeout = setTimeout(this.resize, RESIZE_TIMEOUT); + for (let i = 0; i < SHOULD_UPDATE_ON_PROP_CHANGES.length; i += 1) { + const prop = SHOULD_UPDATE_ON_PROP_CHANGES[i]; + if (nextProps[prop] !== this.props[prop]) { + return true; + } + } + + if ( + nextProps.width !== this.props.width || + nextProps.height !== this.props.height + ) { + clearTimeout(this.resizeTimeout); + this.resizeTimeout = setTimeout(this.resize, RESIZE_TIMEOUT); + } } return false; diff --git a/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx b/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx index 836c0e726d5de..706023a71a2e4 100644 --- a/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx +++ b/superset/assets/src/dashboard/components/gridComponents/ChartHolder.jsx @@ -109,6 +109,7 @@ class ChartHolder extends React.Component { onResizeStop, handleComponentDrop, editMode, + isComponentVisible, } = this.props; // inherit the size of parent columns @@ -163,6 +164,7 @@ class ChartHolder extends React.Component { )} sliceName={component.meta.sliceName || ''} updateSliceName={this.handleUpdateSliceName} + isComponentVisible={isComponentVisible} /> {editMode && ( diff --git a/superset/assets/src/dashboard/components/gridComponents/Column.jsx b/superset/assets/src/dashboard/components/gridComponents/Column.jsx index 7170e4ac864ac..78d272b551c0a 100644 --- a/superset/assets/src/dashboard/components/gridComponents/Column.jsx +++ b/superset/assets/src/dashboard/components/gridComponents/Column.jsx @@ -112,6 +112,7 @@ class Column extends React.PureComponent { onResizeStop, handleComponentDrop, editMode, + isComponentVisible, } = this.props; const columnItems = columnComponent.children || []; @@ -191,6 +192,7 @@ class Column extends React.PureComponent { onResizeStart={onResizeStart} onResize={onResize} onResizeStop={onResizeStop} + isComponentVisible={isComponentVisible} /> ))} diff --git a/superset/assets/src/dashboard/components/gridComponents/Row.jsx b/superset/assets/src/dashboard/components/gridComponents/Row.jsx index 585e57ce91412..f9076bcf4ebf4 100644 --- a/superset/assets/src/dashboard/components/gridComponents/Row.jsx +++ b/superset/assets/src/dashboard/components/gridComponents/Row.jsx @@ -113,6 +113,7 @@ class Row extends React.PureComponent { onResizeStop, handleComponentDrop, editMode, + isComponentVisible, } = this.props; const rowItems = rowComponent.children || []; @@ -177,6 +178,7 @@ class Row extends React.PureComponent { onResizeStart={onResizeStart} onResize={onResize} onResizeStop={onResizeStop} + isComponentVisible={isComponentVisible} /> ))} diff --git a/superset/assets/src/dashboard/components/gridComponents/Tab.jsx b/superset/assets/src/dashboard/components/gridComponents/Tab.jsx index 49a0f187fb5d7..e9e543c80a62f 100644 --- a/superset/assets/src/dashboard/components/gridComponents/Tab.jsx +++ b/superset/assets/src/dashboard/components/gridComponents/Tab.jsx @@ -26,7 +26,6 @@ import AnchorLink from '../../../components/AnchorLink'; import DeleteComponentModal from '../DeleteComponentModal'; import WithPopoverMenu from '../menu/WithPopoverMenu'; import { componentShape } from '../../util/propShapes'; -import { DASHBOARD_ROOT_DEPTH } from '../../util/constants'; export const RENDER_TAB = 'RENDER_TAB'; export const RENDER_TAB_CONTENT = 'RENDER_TAB_CONTENT'; @@ -134,6 +133,7 @@ export default class Tab extends React.PureComponent { onResize, onResizeStop, editMode, + isComponentVisible, } = this.props; return ( @@ -170,6 +170,7 @@ export default class Tab extends React.PureComponent { onResizeStart={onResizeStart} onResize={onResize} onResizeStop={onResizeStop} + isComponentVisible={isComponentVisible} /> ))} {/* Make bottom of tab droppable */} @@ -219,10 +220,6 @@ export default class Tab extends React.PureComponent { index={index} depth={depth} onDrop={this.handleDrop} - // disable drag drop of top-level Tab's to prevent invalid nesting of a child in - // itself, e.g. if a top-level Tab has a Tabs child, dragging the Tab into the Tabs would - // reusult in circular children - disableDragDrop={depth <= DASHBOARD_ROOT_DEPTH + 1} editMode={editMode} > {({ dropIndicatorProps, dragSourceRef }) => ( diff --git a/superset/assets/src/dashboard/components/gridComponents/Tabs.jsx b/superset/assets/src/dashboard/components/gridComponents/Tabs.jsx index 2b8934e9f668c..dfa0cae37f24c 100644 --- a/superset/assets/src/dashboard/components/gridComponents/Tabs.jsx +++ b/superset/assets/src/dashboard/components/gridComponents/Tabs.jsx @@ -238,6 +238,7 @@ class Tabs extends React.PureComponent { onResize={onResize} onResizeStop={onResizeStop} onDropOnTab={this.handleDropOnTab} + isComponentVisible={selectedTabIndex === tabIndex} /> )} diff --git a/superset/assets/src/dashboard/containers/Dashboard.jsx b/superset/assets/src/dashboard/containers/Dashboard.jsx index e5cf4fb59ea0e..c1609a9cfd66c 100644 --- a/superset/assets/src/dashboard/containers/Dashboard.jsx +++ b/superset/assets/src/dashboard/containers/Dashboard.jsx @@ -25,7 +25,7 @@ import { addSliceToDashboard, removeSliceFromDashboard, } from '../actions/dashboardState'; -import { postChartFormData } from '../../chart/chartAction'; +import { triggerQuery } from '../../chart/chartAction'; import { logEvent } from '../../logger/actions'; import getLoadStatsPerTopLevelComponent from '../util/logging/getLoadStatsPerTopLevelComponent'; @@ -64,7 +64,7 @@ function mapDispatchToProps(dispatch) { { addSliceToDashboard, removeSliceFromDashboard, - postChartFormData, + triggerQuery, logEvent, }, dispatch, diff --git a/superset/assets/src/dashboard/containers/DashboardComponent.jsx b/superset/assets/src/dashboard/containers/DashboardComponent.jsx index a1a1c375e957c..2bd306033d6dd 100644 --- a/superset/assets/src/dashboard/containers/DashboardComponent.jsx +++ b/superset/assets/src/dashboard/containers/DashboardComponent.jsx @@ -48,6 +48,7 @@ const propTypes = { const defaultProps = { directPathToChild: [], + isComponentVisible: true, }; function mapStateToProps( diff --git a/superset/assets/src/explore/components/ExploreViewContainer.jsx b/superset/assets/src/explore/components/ExploreViewContainer.jsx index 431f2527a090d..56b5e4319a40f 100644 --- a/superset/assets/src/explore/components/ExploreViewContainer.jsx +++ b/superset/assets/src/explore/components/ExploreViewContainer.jsx @@ -29,7 +29,7 @@ import SaveModal from './SaveModal'; import QueryAndSaveBtns from './QueryAndSaveBtns'; import { getExploreUrlAndPayload, getExploreLongUrl } from '../exploreUtils'; import { areObjectsEqual } from '../../reduxUtils'; -import { getFormDataFromControls } from '../store'; +import { getFormDataFromControls } from '../controlUtils'; import { chartPropShape } from '../../dashboard/util/propShapes'; import * as exploreActions from '../actions/exploreActions'; import * as saveModalActions from '../actions/saveModalActions'; @@ -106,7 +106,6 @@ class ExploreViewContainer extends React.Component { componentWillReceiveProps(nextProps) { if (nextProps.controls.viz_type.value !== this.props.controls.viz_type.value) { this.props.actions.resetControls(); - this.props.actions.triggerQuery(true, this.props.chart.id); } if ( nextProps.controls.datasource && diff --git a/superset/assets/src/explore/controlUtils.js b/superset/assets/src/explore/controlUtils.js new file mode 100644 index 0000000000000..65e8c47ea7921 --- /dev/null +++ b/superset/assets/src/explore/controlUtils.js @@ -0,0 +1,124 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import controlPanelConfigs, { sectionsToRender } from './controlPanels'; +import controls from './controls'; + +export function getFormDataFromControls(controlsState) { + const formData = {}; + Object.keys(controlsState).forEach((controlName) => { + formData[controlName] = controlsState[controlName].value; + }); + return formData; +} + +export function validateControl(control) { + const validators = control.validators; + if (validators && validators.length > 0) { + const validatedControl = { ...control }; + const validationErrors = []; + validators.forEach((f) => { + const v = f(control.value); + if (v) { + validationErrors.push(v); + } + }); + delete validatedControl.validators; + return { ...validatedControl, validationErrors }; + } + return control; +} + +export function getControlKeys(vizType, datasourceType) { + const controlKeys = []; + sectionsToRender(vizType, datasourceType).forEach( + section => section.controlSetRows.forEach( + fieldsetRow => fieldsetRow.forEach( + (field) => { + if (typeof field === 'string') { + controlKeys.push(field); + } + }))); + return controlKeys; +} + +export function getControlConfig(controlKey, vizType) { + // Gets the control definition, applies overrides, and executes + // the mapStatetoProps + const vizConf = controlPanelConfigs[vizType] || {}; + const controlOverrides = vizConf.controlOverrides || {}; + const control = { + ...controls[controlKey], + ...controlOverrides[controlKey], + }; + return control; +} + +export function applyMapStateToPropsToControl(control, state) { + if (control.mapStateToProps) { + const appliedControl = { ...control }; + if (state) { + Object.assign(appliedControl, control.mapStateToProps(state, control)); + } + delete appliedControl.mapStateToProps; + return appliedControl; + } + return control; +} + +function handleMissingChoice(controlKey, control) { + // If the value is not valid anymore based on choices, clear it + const value = control.value; + if ( + control.type === 'SelectControl' && + !control.freeForm && + control.choices && + value + ) { + const alteredControl = { ...control }; + const choiceValues = control.choices.map(c => c[0]); + if (control.multi && value.length > 0) { + alteredControl.value = value.filter(el => choiceValues.indexOf(el) > -1); + return alteredControl; + } else if (!control.multi && choiceValues.indexOf(value) < 0) { + alteredControl.value = null; + return alteredControl; + } + } + return control; +} + +export function getControlState(controlKey, vizType, state, value) { + let controlValue = value; + const controlConfig = getControlConfig(controlKey, vizType); + let controlState = { ...controlConfig }; + controlState = applyMapStateToPropsToControl(controlState, state); + + // If default is a function, evaluate it + if (typeof controlState.default === 'function') { + controlState.default = controlState.default(controlState); + } + + // If a choice control went from multi=false to true, wrap value in array + if (controlConfig.multi && value && !Array.isArray(value)) { + controlValue = [value]; + } + controlState.value = controlValue === undefined ? controlState.default : controlValue; + controlState = handleMissingChoice(controlKey, controlState); + return validateControl(controlState); +} diff --git a/superset/assets/src/explore/controls.jsx b/superset/assets/src/explore/controls.jsx index 95e2b64e9bab7..3c239abefb5e0 100644 --- a/superset/assets/src/explore/controls.jsx +++ b/superset/assets/src/explore/controls.jsx @@ -2095,6 +2095,7 @@ export const controls = { type: 'ColorMapControl', label: t('Color Map'), default: {}, + renderTrigger: true, mapStateToProps: state => ({ colorNamespace: state.form_data.color_namespace, colorScheme: state.form_data.color_scheme, diff --git a/superset/assets/src/explore/reducers/exploreReducer.js b/superset/assets/src/explore/reducers/exploreReducer.js index 6f6a1a9e6b4bd..44d5a72c1ebb4 100644 --- a/superset/assets/src/explore/reducers/exploreReducer.js +++ b/superset/assets/src/explore/reducers/exploreReducer.js @@ -17,8 +17,10 @@ * under the License. */ /* eslint camelcase: 0 */ -import { validateControl, getControlsState, getFormDataFromControls } from '../store'; -import controls from '../controls'; +import { + getControlsState, +} from '../store'; +import { getControlState, getFormDataFromControls } from '../controlUtils'; import * as actions from '../actions/exploreActions'; export default function exploreReducer(state = {}, action) { @@ -78,11 +80,10 @@ export default function exploreReducer(state = {}, action) { [actions.SET_FIELD_VALUE]() { // These errors are reported from the Control components let errors = action.validationErrors || []; - let control = { - ...controls[action.controlName], - value: action.value, + const vizType = state.form_data.viz_type; + const control = { + ...getControlState(action.controlName, vizType, state, action.value), }; - control = validateControl(control); // These errors are based on control config `validators` errors = errors.concat(control.validationErrors || []); diff --git a/superset/assets/src/explore/reducers/getInitialState.js b/superset/assets/src/explore/reducers/getInitialState.js index 98b979914a085..892224de57f72 100644 --- a/superset/assets/src/explore/reducers/getInitialState.js +++ b/superset/assets/src/explore/reducers/getInitialState.js @@ -20,7 +20,8 @@ import shortid from 'shortid'; import getToastsFromPyFlashMessages from '../../messageToasts/utils/getToastsFromPyFlashMessages'; import { getChartKey } from '../exploreUtils'; -import { getControlsState, getFormDataFromControls } from '../store'; +import { getControlsState } from '../store'; +import { getFormDataFromControls } from '../controlUtils'; export default function getInitialState(bootstrapData) { const controls = getControlsState(bootstrapData, bootstrapData.form_data); diff --git a/superset/assets/src/explore/store.js b/superset/assets/src/explore/store.js index df456c29ebc94..6d58b8d04055d 100644 --- a/superset/assets/src/explore/store.js +++ b/superset/assets/src/explore/store.js @@ -17,44 +17,13 @@ * under the License. */ /* eslint camelcase: 0 */ -import React from 'react'; +import { + getControlState, + getControlKeys, + getFormDataFromControls, +} from './controlUtils'; import controls from './controls'; -import controlPanelConfigs, { sectionsToRender } from './controlPanels'; - -export function getFormDataFromControls(controlsState) { - const formData = {}; - Object.keys(controlsState).forEach((controlName) => { - formData[controlName] = controlsState[controlName].value; - }); - return formData; -} - -export function validateControl(control) { - const validators = control.validators; - const validationErrors = []; - if (validators && validators.length > 0) { - validators.forEach((f) => { - const v = f(control.value); - if (v) { - validationErrors.push(v); - } - }); - } - if (validationErrors.length > 0) { - return { ...control, validationErrors }; - } - return control; -} - - -export function getControlNames(vizType, datasourceType) { - const controlNames = []; - sectionsToRender(vizType, datasourceType).forEach( - section => section.controlSetRows.forEach( - fsr => fsr.forEach( - f => controlNames.push(f)))); - return controlNames; -} +import controlPanelConfigs from './controlPanels'; function handleDeprecatedControls(formData) { // Reacffectation / handling of deprecated controls @@ -66,104 +35,54 @@ function handleDeprecatedControls(formData) { } } -export function getControlsState(state, form_data) { +export function getControlsState(state, inputFormData) { /* * Gets a new controls object to put in the state. The controls object * is similar to the configuration control with only the controls * related to the current viz_type, materializes mapStateToProps functions, - * adds value keys coming from form_data passed here. This can't be an action creator + * adds value keys coming from inputFormData passed here. This can't be an action creator * just yet because it's used in both the explore and dashboard views. * */ // Getting a list of active control names for the current viz - const formData = Object.assign({}, form_data); + const formData = Object.assign({}, inputFormData); const vizType = formData.viz_type || 'table'; handleDeprecatedControls(formData); - const controlNames = getControlNames(vizType, state.datasource.type); + const controlNames = getControlKeys(vizType, state.datasource.type); const viz = controlPanelConfigs[vizType] || {}; - const controlOverrides = viz.controlOverrides || {}; const controlsState = {}; - controlNames.forEach((k) => { - if (React.isValidElement(k)) { - // no state - return; - } - const control = Object.assign({}, controls[k], controlOverrides[k]); - if (control.mapStateToProps) { - Object.assign(control, control.mapStateToProps(state, control)); - delete control.mapStateToProps; - } - - formData[k] = (control.multi && formData[k] && !Array.isArray(formData[k])) ? [formData[k]] - : formData[k]; - - // If the value is not valid anymore based on choices, clear it - if ( - control.type === 'SelectControl' && - !control.freeForm && - control.choices && - k !== 'datasource' && - formData[k] - ) { - const choiceValues = control.choices.map(c => c[0]); - if (control.multi && formData[k].length > 0) { - formData[k] = formData[k].filter(el => choiceValues.indexOf(el) > -1); - } else if (!control.multi && choiceValues.indexOf(formData[k]) < 0) { - delete formData[k]; - } - } - if (typeof control.default === 'function') { - control.default = control.default(control); - } - control.validationErrors = []; - control.value = control.default; - // formData[k]'s type should match control value type - if (formData[k] !== undefined && - (Array.isArray(formData[k]) && control.multi || !control.multi) - ) { - control.value = formData[k]; - } - controlsState[k] = validateControl(control); + controlNames.forEach((k) => { + const control = getControlState(k, vizType, state, formData[k]); + controlsState[k] = control; + formData[k] = control.value; }); + if (viz.onInit) { return viz.onInit(controlsState); } return controlsState; } -export function applyDefaultFormData(form_data) { - const datasourceType = form_data.datasource.split('__')[1]; - const vizType = form_data.viz_type || 'table'; - const viz = controlPanelConfigs[vizType] || {}; - const controlNames = getControlNames(vizType, datasourceType); - const controlOverrides = viz.controlOverrides || {}; +export function applyDefaultFormData(inputFormData) { + const datasourceType = inputFormData.datasource.split('__')[1]; + const vizType = inputFormData.viz_type; + const controlNames = getControlKeys(vizType, datasourceType); const formData = {}; controlNames.forEach((k) => { - const control = Object.assign({}, controls[k]); - if (controlOverrides[k]) { - Object.assign(control, controlOverrides[k]); - } - if (form_data[k] === undefined) { - if (typeof control.default === 'function') { - formData[k] = control.default(controls[k]); - } else { - formData[k] = control.default; - } + const controlState = getControlState(k, vizType, null, inputFormData[k]); + if (inputFormData[k] === undefined) { + formData[k] = controlState.value; } else { - formData[k] = form_data[k]; + formData[k] = inputFormData[k]; } }); return formData; } -export const autoQueryControls = [ - 'datasource', - 'viz_type', -]; const defaultControls = Object.assign({}, controls); Object.keys(controls).forEach((f) => { diff --git a/superset/assets/src/featureFlags.ts b/superset/assets/src/featureFlags.ts index bd0855ef18461..450ad2cd4f896 100644 --- a/superset/assets/src/featureFlags.ts +++ b/superset/assets/src/featureFlags.ts @@ -23,7 +23,6 @@ export enum FeatureFlag { OMNIBAR = 'OMNIBAR', CLIENT_CACHE = 'CLIENT_CACHE', SCHEDULED_QUERIES = 'SCHEDULED_QUERIES', - SQL_VALIDATORS_BY_ENGINE = 'SQL_VALIDATORS_BY_ENGINE', } export type FeatureFlagMap = { diff --git a/superset/assets/src/visualizations/deckgl/utils.js b/superset/assets/src/visualizations/deckgl/utils.js index b2b130a4835ee..62024efa0d1fc 100644 --- a/superset/assets/src/visualizations/deckgl/utils.js +++ b/superset/assets/src/visualizations/deckgl/utils.js @@ -34,6 +34,9 @@ export function getBreakPoints({ // compute evenly distributed break points based on number of buckets const numBuckets = formDataNumBuckets ? parseInt(formDataNumBuckets, 10) : DEFAULT_NUM_BUCKETS; const [minValue, maxValue] = extent(features, accessor); + if (minValue === undefined) { + return []; + } const delta = (maxValue - minValue) / numBuckets; const precision = delta === 0 ? 0 diff --git a/superset/cli.py b/superset/cli.py index 7f5fe1773f879..52429104fbf79 100755 --- a/superset/cli.py +++ b/superset/cli.py @@ -24,11 +24,10 @@ import click from colorama import Fore, Style from pathlib2 import Path -import werkzeug.serving import yaml from superset import ( - app, data, db, security_manager, + app, appbuilder, data, db, security_manager, ) from superset.utils import ( core as utils, dashboard_import_export, dict_import_export) @@ -50,100 +49,10 @@ def make_shell_context(): def init(): """Inits the Superset application""" utils.get_or_create_main_db() + appbuilder.add_permissions(update_perms=True) security_manager.sync_role_definitions() -def debug_run(app, port, use_reloader): - click.secho( - '[DEPRECATED] As of Flask >=1.0.0, this command is no longer ' - 'supported, please use `flask run` instead, as documented in our ' - 'CONTRIBUTING.md', - fg='red', - ) - click.secho('[example]', fg='yellow') - click.secho( - 'flask run -p 8080 --with-threads --reload --debugger', - fg='green', - ) - - -def console_log_run(app, port, use_reloader): - from console_log import ConsoleLog - from gevent import pywsgi - from geventwebsocket.handler import WebSocketHandler - - app.wsgi_app = ConsoleLog(app.wsgi_app, app.logger) - - def run(): - server = pywsgi.WSGIServer( - ('0.0.0.0', int(port)), - app, - handler_class=WebSocketHandler) - server.serve_forever() - - if use_reloader: - from gevent import monkey - monkey.patch_all() - run = werkzeug.serving.run_with_reloader(run) - - run() - - -@app.cli.command() -@click.option('--debug', '-d', is_flag=True, help='Start the web server in debug mode') -@click.option('--console-log', is_flag=True, - help='Create logger that logs to the browser console (implies -d)') -@click.option('--no-reload', '-n', 'use_reloader', flag_value=False, - default=config.get('FLASK_USE_RELOAD'), - help='Don\'t use the reloader in debug mode') -@click.option('--address', '-a', default=config.get('SUPERSET_WEBSERVER_ADDRESS'), - help='Specify the address to which to bind the web server') -@click.option('--port', '-p', default=config.get('SUPERSET_WEBSERVER_PORT'), - help='Specify the port on which to run the web server') -@click.option('--workers', '-w', default=config.get('SUPERSET_WORKERS', 2), - help='Number of gunicorn web server workers to fire up [DEPRECATED]') -@click.option('--timeout', '-t', default=config.get('SUPERSET_WEBSERVER_TIMEOUT'), - help='Specify the timeout (seconds) for the ' - 'gunicorn web server [DEPRECATED]') -@click.option('--socket', '-s', default=config.get('SUPERSET_WEBSERVER_SOCKET'), - help='Path to a UNIX socket as an alternative to address:port, e.g. ' - '/var/run/superset.sock. ' - 'Will override the address and port values. [DEPRECATED]') -def runserver(debug, console_log, use_reloader, address, port, timeout, workers, socket): - """Starts a Superset web server.""" - debug = debug or config.get('DEBUG') or console_log - if debug: - print(Fore.BLUE + '-=' * 20) - print( - Fore.YELLOW + 'Starting Superset server in ' + - Fore.RED + 'DEBUG' + - Fore.YELLOW + ' mode') - print(Fore.BLUE + '-=' * 20) - print(Style.RESET_ALL) - if console_log: - console_log_run(app, port, use_reloader) - else: - debug_run(app, port, use_reloader) - else: - logging.info( - "The Gunicorn 'superset runserver' command is deprecated. Please " - "use the 'gunicorn' command instead.") - addr_str = f' unix:{socket} ' if socket else f' {address}:{port} ' - cmd = ( - 'gunicorn ' - f'-w {workers} ' - f'--timeout {timeout} ' - f'-b {addr_str} ' - '--limit-request-line 0 ' - '--limit-request-field_size 0 ' - 'superset:app' - ) - print(Fore.GREEN + 'Starting server with command: ') - print(Fore.YELLOW + cmd) - print(Style.RESET_ALL) - Popen(cmd, shell=True).wait() - - @app.cli.command() @click.option('--verbose', '-v', is_flag=True, help='Show extra information') def version(verbose): @@ -208,6 +117,9 @@ def load_examples_run(load_test_data): print('Loading DECK.gl demo') data.load_deck_dash() + print('Loading [Tabbed dashboard]') + data.load_tabbed_dashboard() + @app.cli.command() @click.option('--load-test-data', '-t', is_flag=True, help='Load additional test data') diff --git a/superset/connectors/druid/models.py b/superset/connectors/druid/models.py index 43a092c20c548..17ec4b82ebcf8 100644 --- a/superset/connectors/druid/models.py +++ b/superset/connectors/druid/models.py @@ -1118,7 +1118,8 @@ def run_query( # noqa / druid columns.append('__time') del qry['post_aggregations'] del qry['aggregations'] - qry['dimensions'] = columns + del qry['dimensions'] + qry['columns'] = columns qry['metrics'] = [] qry['granularity'] = 'all' qry['limit'] = row_limit diff --git a/superset/data/__init__.py b/superset/data/__init__.py index 5090effe5fc42..b36a3002f1f3e 100644 --- a/superset/data/__init__.py +++ b/superset/data/__init__.py @@ -28,5 +28,6 @@ from .paris import load_paris_iris_geojson # noqa from .random_time_series import load_random_time_series_data # noqa from .sf_population_polygons import load_sf_population_polygons # noqa +from .tabbed_dashboard import load_tabbed_dashboard # noqa from .unicode_test_data import load_unicode_test_data # noqa from .world_bank import load_world_bank_health_n_pop # noqa diff --git a/superset/data/tabbed_dashboard.py b/superset/data/tabbed_dashboard.py new file mode 100644 index 0000000000000..4c81f8563e0c6 --- /dev/null +++ b/superset/data/tabbed_dashboard.py @@ -0,0 +1,324 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +"""Loads datasets, dashboards and slices in a new superset instance""" +# pylint: disable=C,R,W +import json +import os +import textwrap + +import pandas as pd +from sqlalchemy import DateTime, String + +from superset import db +from superset.connectors.sqla.models import SqlMetric +from superset.utils import core as utils +from .helpers import ( + config, + Dash, + DATA_FOLDER, + get_example_data, + get_slice_json, + merge_slice, + misc_dash_slices, + Slice, + TBL, + update_slice_ids, +) + + +def load_tabbed_dashboard(): + """Creating a tabbed dashboard""" + + print("Creating a dashboard with nested tabs") + slug = 'tabbed_dash' + dash = db.session.query(Dash).filter_by(slug=slug).first() + + if not dash: + dash = Dash() + + # reuse charts in "World's Bank Data and create + # new dashboard with nested tabs + tabbed_dash_slices = set() + tabbed_dash_slices.add('Region Filter') + tabbed_dash_slices.add('Growth Rate') + tabbed_dash_slices.add('Treemap') + tabbed_dash_slices.add('Box plot') + + js = textwrap.dedent("""\ + { + "CHART-c0EjR-OZ0n": { + "children": [], + "id": "CHART-c0EjR-OZ0n", + "meta": { + "chartId": 870, + "height": 50, + "sliceName": "Box plot", + "width": 4 + }, + "parents": [ + "ROOT_ID", + "TABS-lV0r00f4H1", + "TAB-NF3dlrWGS", + "ROW-7G2o5uDvfo" + ], + "type": "CHART" + }, + "CHART-dxV7Il74hH": { + "children": [], + "id": "CHART-dxV7Il74hH", + "meta": { + "chartId": 797, + "height": 50, + "sliceName": "Treemap", + "width": 4 + }, + "parents": [ + "ROOT_ID", + "TABS-lV0r00f4H1", + "TAB-gcQJxApOZS", + "ROW-3PphCz4GD" + ], + "type": "CHART" + }, + "CHART-jJ5Yj1Ptaz": { + "children": [], + "id": "CHART-jJ5Yj1Ptaz", + "meta": { + "chartId": 789, + "height": 50, + "sliceName": "World's Population", + "width": 4 + }, + "parents": [ + "ROOT_ID", + "TABS-lV0r00f4H1", + "TAB-NF3dlrWGS", + "TABS-CSjo6VfNrj", + "TAB-z81Q87PD7", + "ROW-G73z9PIHn" + ], + "type": "CHART" + }, + "CHART-z4gmEuCqQ5": { + "children": [], + "id": "CHART-z4gmEuCqQ5", + "meta": { + "chartId": 788, + "height": 50, + "sliceName": "Region Filter", + "width": 4 + }, + "parents": [ + "ROOT_ID", + "TABS-lV0r00f4H1", + "TAB-NF3dlrWGS", + "TABS-CSjo6VfNrj", + "TAB-EcNm_wh922", + "ROW-LCjsdSetJ" + ], + "type": "CHART" + }, + "DASHBOARD_VERSION_KEY": "v2", + "GRID_ID": { + "children": [], + "id": "GRID_ID", + "type": "GRID" + }, + "HEADER_ID": { + "id": "HEADER_ID", + "meta": { + "text": "Tabbed Dashboard" + }, + "type": "HEADER" + }, + "ROOT_ID": { + "children": [ + "TABS-lV0r00f4H1" + ], + "id": "ROOT_ID", + "type": "ROOT" + }, + "ROW-3PphCz4GD": { + "children": [ + "CHART-dxV7Il74hH" + ], + "id": "ROW-3PphCz4GD", + "meta": { + "background": "BACKGROUND_TRANSPARENT" + }, + "parents": [ + "ROOT_ID", + "TABS-lV0r00f4H1", + "TAB-gcQJxApOZS" + ], + "type": "ROW" + }, + "ROW-7G2o5uDvfo": { + "children": [ + "CHART-c0EjR-OZ0n" + ], + "id": "ROW-7G2o5uDvfo", + "meta": { + "background": "BACKGROUND_TRANSPARENT" + }, + "parents": [ + "ROOT_ID", + "TABS-lV0r00f4H1", + "TAB-NF3dlrWGS" + ], + "type": "ROW" + }, + "ROW-G73z9PIHn": { + "children": [ + "CHART-jJ5Yj1Ptaz" + ], + "id": "ROW-G73z9PIHn", + "meta": { + "background": "BACKGROUND_TRANSPARENT" + }, + "parents": [ + "ROOT_ID", + "TABS-lV0r00f4H1", + "TAB-NF3dlrWGS", + "TABS-CSjo6VfNrj", + "TAB-z81Q87PD7" + ], + "type": "ROW" + }, + "ROW-LCjsdSetJ": { + "children": [ + "CHART-z4gmEuCqQ5" + ], + "id": "ROW-LCjsdSetJ", + "meta": { + "background": "BACKGROUND_TRANSPARENT" + }, + "parents": [ + "ROOT_ID", + "TABS-lV0r00f4H1", + "TAB-NF3dlrWGS", + "TABS-CSjo6VfNrj", + "TAB-EcNm_wh922" + ], + "type": "ROW" + }, + "TAB-EcNm_wh922": { + "children": [ + "ROW-LCjsdSetJ" + ], + "id": "TAB-EcNm_wh922", + "meta": { + "text": "row tab 1" + }, + "parents": [ + "ROOT_ID", + "TABS-lV0r00f4H1", + "TAB-NF3dlrWGS", + "TABS-CSjo6VfNrj" + ], + "type": "TAB" + }, + "TAB-NF3dlrWGS": { + "children": [ + "ROW-7G2o5uDvfo", + "TABS-CSjo6VfNrj" + ], + "id": "TAB-NF3dlrWGS", + "meta": { + "text": "Tab A" + }, + "parents": [ + "ROOT_ID", + "TABS-lV0r00f4H1" + ], + "type": "TAB" + }, + "TAB-gcQJxApOZS": { + "children": [ + "ROW-3PphCz4GD" + ], + "id": "TAB-gcQJxApOZS", + "meta": { + "text": "Tab B" + }, + "parents": [ + "ROOT_ID", + "TABS-lV0r00f4H1" + ], + "type": "TAB" + }, + "TAB-z81Q87PD7": { + "children": [ + "ROW-G73z9PIHn" + ], + "id": "TAB-z81Q87PD7", + "meta": { + "text": "row tab 2" + }, + "parents": [ + "ROOT_ID", + "TABS-lV0r00f4H1", + "TAB-NF3dlrWGS", + "TABS-CSjo6VfNrj" + ], + "type": "TAB" + }, + "TABS-CSjo6VfNrj": { + "children": [ + "TAB-EcNm_wh922", + "TAB-z81Q87PD7" + ], + "id": "TABS-CSjo6VfNrj", + "meta": {}, + "parents": [ + "ROOT_ID", + "TABS-lV0r00f4H1", + "TAB-NF3dlrWGS" + ], + "type": "TABS" + }, + "TABS-lV0r00f4H1": { + "children": [ + "TAB-NF3dlrWGS", + "TAB-gcQJxApOZS" + ], + "id": "TABS-lV0r00f4H1", + "meta": {}, + "parents": [ + "ROOT_ID" + ], + "type": "TABS" + } + } + """) + pos = json.loads(js) + slices = [ + db.session.query(Slice) + .filter_by(slice_name=name) + .first() + for name in tabbed_dash_slices + ] + + slices = sorted(slices, key=lambda x: x.id) + update_slice_ids(pos, slices) + dash.position_json = json.dumps(pos, indent=4) + dash.slices = slices + dash.dashboard_title = 'Tabbed Dashboard' + dash.slug = slug + + db.session.merge(dash) + db.session.commit() diff --git a/superset/data/world_bank.py b/superset/data/world_bank.py index 16aa0cb24c656..94aa468ccd3c1 100644 --- a/superset/data/world_bank.py +++ b/superset/data/world_bank.py @@ -71,7 +71,7 @@ def load_world_bank_health_n_pop(): metrics = [ 'sum__SP_POP_TOTL', 'sum__SH_DYN_AIDS', 'sum__SH_DYN_AIDS', - 'sum__SP_RUR_TOTL_ZS', 'sum__SP_DYN_LE00_IN', + 'sum__SP_RUR_TOTL_ZS', 'sum__SP_DYN_LE00_IN', 'sum__SP_RUR_TOTL' ] for m in metrics: if not any(col.metric_name == m for col in tbl.metrics): diff --git a/superset/db_engine_specs.py b/superset/db_engine_specs.py index 5fed48049107f..35a591fa10202 100644 --- a/superset/db_engine_specs.py +++ b/superset/db_engine_specs.py @@ -37,6 +37,7 @@ import textwrap import time from typing import List, Tuple +from urllib import parse from flask import g from flask_babel import lazy_gettext as _ @@ -577,6 +578,7 @@ def adjust_database_uri(cls, uri, selected_schema=None): if '/' in uri.database: database = uri.database.split('/')[0] if selected_schema: + selected_schema = parse.quote(selected_schema, safe='') uri.database = database + '/' + selected_schema return uri @@ -757,7 +759,7 @@ def convert_dttm(cls, target_type, dttm): @classmethod def adjust_database_uri(cls, uri, selected_schema=None): if selected_schema: - uri.database = selected_schema + uri.database = parse.quote(selected_schema, safe='') return uri @classmethod @@ -1081,6 +1083,7 @@ def select_star(cls, my_db, table_name: str, engine: Engine, schema: str = None, def adjust_database_uri(cls, uri, selected_schema=None): database = uri.database if selected_schema and database: + selected_schema = parse.quote(selected_schema, safe='') if '/' in database: database = database.split('/')[0] + '/' + selected_schema else: @@ -1484,7 +1487,7 @@ def convert_dttm(cls, target_type, dttm): @classmethod def adjust_database_uri(cls, uri, selected_schema=None): if selected_schema: - uri.database = selected_schema + uri.database = parse.quote(selected_schema, safe='') return uri @classmethod diff --git a/superset/models/annotations.py b/superset/models/annotations.py index 027f14f9bad97..1de9dfd2f97c0 100644 --- a/superset/models/annotations.py +++ b/superset/models/annotations.py @@ -46,7 +46,7 @@ class Annotation(Model, AuditMixinNullable): id = Column(Integer, primary_key=True) start_dttm = Column(DateTime) end_dttm = Column(DateTime) - layer_id = Column(Integer, ForeignKey('annotation_layer.id')) + layer_id = Column(Integer, ForeignKey('annotation_layer.id'), nullable=False) short_descr = Column(String(500)) long_descr = Column(Text) layer = relationship( diff --git a/superset/sql_parse.py b/superset/sql_parse.py index 2f65392558f9e..662f6c326229b 100644 --- a/superset/sql_parse.py +++ b/superset/sql_parse.py @@ -18,7 +18,7 @@ import logging import sqlparse -from sqlparse.sql import Identifier, IdentifierList +from sqlparse.sql import Identifier, IdentifierList, Token, TokenList from sqlparse.tokens import Keyword, Name RESULT_OPERATIONS = {'UNION', 'INTERSECT', 'EXCEPT', 'SELECT'} @@ -75,32 +75,32 @@ def get_statements(self): return statements @staticmethod - def __get_full_name(identifier): - if len(identifier.tokens) > 2 and identifier.tokens[1].value == '.': - return '{}.{}'.format(identifier.tokens[0].value, - identifier.tokens[2].value) - return identifier.get_real_name() + def __get_full_name(tlist: TokenList): + if len(tlist.tokens) > 2 and tlist.tokens[1].value == '.': + return '{}.{}'.format(tlist.tokens[0].value, + tlist.tokens[2].value) + return tlist.get_real_name() @staticmethod - def __is_identifier(token): + def __is_identifier(token: Token): return isinstance(token, (IdentifierList, Identifier)) - def __process_identifier(self, identifier): + def __process_tokenlist(self, tlist: TokenList): # exclude subselects - if '(' not in str(identifier): - table_name = self.__get_full_name(identifier) + if '(' not in str(tlist): + table_name = self.__get_full_name(tlist) if table_name and not table_name.startswith(CTE_PREFIX): self._table_names.add(table_name) return # store aliases - if hasattr(identifier, 'get_alias'): - self._alias_names.add(identifier.get_alias()) - if hasattr(identifier, 'tokens'): - # some aliases are not parsed properly - if identifier.tokens[0].ttype == Name: - self._alias_names.add(identifier.tokens[0].value) - self.__extract_from_token(identifier) + if tlist.has_alias(): + self._alias_names.add(tlist.get_alias()) + + # some aliases are not parsed properly + if tlist.tokens[0].ttype == Name: + self._alias_names.add(tlist.tokens[0].value) + self.__extract_from_token(tlist) def as_create_table(self, table_name, overwrite=False): """Reformats the query into the create table as query. @@ -144,10 +144,11 @@ def __extract_from_token(self, token, depth=0): if table_name_preceding_token: if isinstance(item, Identifier): - self.__process_identifier(item) + self.__process_tokenlist(item) elif isinstance(item, IdentifierList): for token in item.get_identifiers(): - self.__process_identifier(token) + if isinstance(token, TokenList): + self.__process_tokenlist(token) elif isinstance(item, IdentifierList): for token in item.tokens: if not self.__is_identifier(token): diff --git a/superset/sql_validators/presto_db.py b/superset/sql_validators/presto_db.py index 87c2d8efeb805..8f2be4389ff6c 100644 --- a/superset/sql_validators/presto_db.py +++ b/superset/sql_validators/presto_db.py @@ -26,7 +26,6 @@ ) from flask import g -from pyhive.exc import DatabaseError from superset import app, security_manager from superset.sql_parse import ParsedQuery @@ -77,6 +76,7 @@ def validate_statement( # engine spec's handle_cursor implementation since we don't record # these EXPLAIN queries done in validation as proper Query objects # in the superset ORM. + from pyhive.exc import DatabaseError try: db_engine_spec.execute(cursor, sql) polled = cursor.poll() diff --git a/superset/templates/superset/export_dashboards.html b/superset/templates/superset/export_dashboards.html index 204ac2c3a7667..5ec6ff1643e12 100644 --- a/superset/templates/superset/export_dashboards.html +++ b/superset/templates/superset/export_dashboards.html @@ -18,7 +18,16 @@ #} diff --git a/superset/translations/de/LC_MESSAGES/messages.json b/superset/translations/de/LC_MESSAGES/messages.json index ca4eadbd9f548..0a71b27c6df34 100644 --- a/superset/translations/de/LC_MESSAGES/messages.json +++ b/superset/translations/de/LC_MESSAGES/messages.json @@ -1,2415 +1,4 @@ -{ - "domain": "superset", - "locale_data": { - "superset": { - "": { - "domain": "superset", - "plural_forms": "nplurals=2; plural=(n != 1)", - "lang": "de" - }, - "Time Column": [ - "Zeitspalte" - ], - "second": [ - "Sekunde" - ], - "minute": [ - "Minute" - ], - "hour": [ - "Stunde" - ], - "day": [ - "Tag" - ], - "week": [ - "Woche" - ], - "month": [ - "Monat" - ], - "quarter": [ - "Vierteljahr" - ], - "year": [ - "Jahr" - ], - "week_start_monday": [ - "" - ], - "week_ending_saturday": [ - "" - ], - "week_start_sunday": [ - "" - ], - "5 minute": [ - "5 Minuten" - ], - "half hour": [ - "halbe Stunde" - ], - "10 minute": [ - "10 Minuten" - ], - "[Superset] Access to the datasource %(name)s was granted": [ - "[Superset] Zugriff auf Datenquelle %(name)s war genehmigt " - ], - "Viz fehlt eine Datenquelle": [ - "" - ], - "From date cannot be larger than to date": [ - "'Von Datum' kann nicht größer als 'bis Datum' sein" - ], - "Table View": [ - "Tabellenansicht" - ], - "Pick a granularity in the Time section or uncheck 'Include Time'": [ - "" - ], - "Choose either fields to [Group By] and [Metrics] or [Columns], not both": [ - "" - ], - "Pivot Table": [ - "" - ], - "Please choose at least one \"Group by\" field ": [ - "" - ], - "Please choose at least one metric": [ - "" - ], - "'Group By' and 'Columns' can't overlap": [ - "" - ], - "Markup": [ - "" - ], - "Separator": [ - "" - ], - "Word Cloud": [ - "" - ], - "Treemap": [ - "" - ], - "Calendar Heatmap": [ - "" - ], - "Box Plot": [ - "" - ], - "Bubble Chart": [ - "" - ], - "Pick a metric for x, y and size": [ - "" - ], - "Bullet Chart": [ - "" - ], - "Pick a metric to display": [ - "" - ], - "Big Number with Trendline": [ - "" - ], - "Pick a metric!": [ - "" - ], - "Big Number": [ - "" - ], - "Time Series - Line Chart": [ - "" - ], - "Pick a time granularity for your time series": [ - "" - ], - "Time Series - Dual Axis Line Chart": [ - "" - ], - "Pick a metric for left axis!": [ - "" - ], - "Pick a metric for right axis!": [ - "" - ], - "Please choose different metrics on left and right axis": [ - "" - ], - "Time Series - Bar Chart": [ - "" - ], - "Time Series - Percent Change": [ - "" - ], - "Time Series - Stacked": [ - "" - ], - "Distribution - NVD3 - Pie Chart": [ - "" - ], - "Histogram": [ - "Histogramm" - ], - "Must have one numeric column specified": [ - "" - ], - "Distribution - Bar Chart": [ - "" - ], - "Can't have overlap between Series and Breakdowns": [ - "" - ], - "Pick at least one metric": [ - "" - ], - "Pick at least one field for [Series]": [ - "" - ], - "Sunburst": [ - "" - ], - "Sankey": [ - "" - ], - "Pick exactly 2 columns as [Source / Target]": [ - "" - ], - "There's a loop in your Sankey, please provide a tree. Here's a faulty link: {}": [ - "" - ], - "Directed Force Layout": [ - "" - ], - "Pick exactly 2 columns to 'Group By'": [ - "" - ], - "Country Map": [ - "" - ], - "World Map": [ - "" - ], - "Filters": [ - "" - ], - "Pick at least one filter field": [ - "" - ], - "iFrame": [ - "" - ], - "Parallel Coordinates": [ - "" - ], - "Heatmap": [ - "" - ], - "Horizon Charts": [ - "" - ], - "Mapbox": [ - "" - ], - "Must have a [Group By] column to have 'count' as the [Label]": [ - "" - ], - "Choice of [Label] must be present in [Group By]": [ - "" - ], - "Choice of [Point Radius] must be present in [Group By]": [ - "" - ], - "[Longitude] and [Latitude] columns must be present in [Group By]": [ - "" - ], - "Event flow": [ - "" - ], - "Time Series - Paired t-test": [ - "" - ], - "Your query was saved": [ - "Ihre Abfrage wurde gespeichert" - ], - "Your query could not be saved": [ - "Ihre Abfrage konnte nicht gespeichert werden" - ], - "Failed at retrieving results from the results backend": [ - "" - ], - "Could not connect to server": [ - "" - ], - "Your session timed out, please refresh your page and try again.": [ - "" - ], - "Query was stopped.": [ - "" - ], - "Failed at stopping query.": [ - "" - ], - "Error occurred while fetching table metadata": [ - "" - ], - "shared query": [ - "" - ], - "The query couldn't be loaded": [ - "" - ], - "An error occurred while creating the data source": [ - "" - ], - "Pick a chart type!": [ - "" - ], - "To use this chart type you need at least one column flagged as a date": [ - "" - ], - "To use this chart type you need at least one dimension": [ - "" - ], - "To use this chart type you need at least one aggregation function": [ - "" - ], - "Untitled Query": [ - "" - ], - "Copy of %s": [ - "" - ], - "share query": [ - "" - ], - "copy URL to clipboard": [ - "" - ], - "Raw SQL": [ - "" - ], - "Source SQL": [ - "" - ], - "SQL": [ - "" - ], - "No query history yet...": [ - "" - ], - "It seems you don't have access to any database": [ - "" - ], - "Search Results": [ - "" - ], - "[From]-": [ - "" - ], - "[To]-": [ - "" - ], - "[Query Status]": [ - "" - ], - "Search": [ - "Suche" - ], - "Open in SQL Editor": [ - "Bearbeiten in SQL Editor" - ], - "view results": [ - "" - ], - "Data preview": [ - "" - ], - "Visualize the data out of this query": [ - "" - ], - "Overwrite text in editor with a query on this table": [ - "" - ], - "Run query in a new tab": [ - "" - ], - "Remove query from log": [ - "" - ], - ".CSV": [ - "" - ], - "Visualize": [ - "Visualalisieren" - ], - "Table": [ - "Tabellen" - ], - "was created": [ - "" - ], - "Query in a new tab": [ - "" - ], - "Fetch data preview": [ - "" - ], - "Track Job": [ - "" - ], - "Loading...": [ - "" - ], - "Run Selected Query": [ - "" - ], - "Run Query": [ - "" - ], - "Run query asynchronously": [ - "" - ], - "Stop": [ - "Stopp" - ], - "Undefined": [ - "" - ], - "Label": [ - "" - ], - "Label for your query": [ - "" - ], - "Description": [ - "" - ], - "Write a description for your query": [ - "" - ], - "Save": [ - "" - ], - "Cancel": [ - "" - ], - "Save Query": [ - "" - ], - "Run a query to display results here": [ - "" - ], - "Preview for %s": [ - "" - ], - "Results": [ - "" - ], - "Query History": [ - "" - ], - "Create table as with query results": [ - "" - ], - "new table name": [ - "" - ], - "Error while fetching table list": [ - "" - ], - "Error while fetching schema list": [ - "" - ], - "Error while fetching database list": [ - "" - ], - "Database:": [ - "" - ], - "Select a database": [ - "" - ], - "Select a schema (%s)": [ - "" - ], - "Schema:": [ - "" - ], - "Add a table (%s)": [ - "" - ], - "Type to search ...": [ - "" - ], - "Reset State": [ - "" - ], - "Enter a new title for the tab": [ - "" - ], - "Untitled Query %s": [ - "" - ], - "close tab": [ - "" - ], - "rename tab": [ - "" - ], - "expand tool bar": [ - "" - ], - "hide tool bar": [ - "" - ], - "Copy partition query to clipboard": [ - "" - ], - "latest partition:": [ - "" - ], - "Keys for table": [ - "" - ], - "View keys & indexes (%s)": [ - "" - ], - "Sort columns alphabetically": [ - "" - ], - "Original table column order": [ - "" - ], - "Copy SELECT statement to clipboard": [ - "" - ], - "Remove table preview": [ - "" - ], - "%s is not right as a column name, please alias it (as in SELECT count(*) ": [ - "" - ], - "AS my_alias": [ - "" - ], - "using only alphanumeric characters and underscores": [ - "" - ], - "Creating a data source and popping a new tab": [ - "" - ], - "No results available for this query": [ - "" - ], - "Chart Type": [ - "" - ], - "[Chart Type]": [ - "" - ], - "Datasource Name": [ - "" - ], - "datasource name": [ - "" - ], - "Select ...": [ - "" - ], - "Loaded data cached": [ - "" - ], - "Loaded from cache": [ - "" - ], - "Click to force-refresh": [ - "" - ], - "Copy to clipboard": [ - "" - ], - "Not successful": [ - "" - ], - "Sorry, your browser does not support copying. Use Ctrl / Cmd + C!": [ - "" - ], - "Copied!": [ - "" - ], - "Title": [ - "Titel" - ], - "click to edit title": [ - "" - ], - "You don't have the rights to alter this title.": [ - "" - ], - "Click to favorite/unfavorite": [ - "" - ], - "You have unsaved changes.": [ - "" - ], - "Click the": [ - "" - ], - "button on the top right to save your changes.": [ - "" - ], - "Served from data cached %s . Click to force refresh.": [ - "Von Daten dienten um %s im Cache gespeichert. Aktualisierung erzwingen durch Klicken" - ], - "Click to force refresh": [ - "Aktualisierung erzwingen" - ], - "Error": [ - "Fehler" - ], - "Sorry, there was an error adding slices to this dashboard: %s": [ - "" - ], - "Active Dashboard Filters": [ - "" - ], - "Checkout this dashboard: %s": [ - "" - ], - "Force refresh the whole dashboard": [ - "Ganze Dashboard Aktualisierung erzwingen" - ], - "Edit this dashboard's properties": [ - "Dashboardeigenschaften bearbeiten" - ], - "Load a template": [ - "Vorlage laden" - ], - "Load a CSS template": [ - "CSS Vorlage laden" - ], - "CSS": [ - "CSS" - ], - "Live CSS Editor": [ - "" - ], - "Don't refresh": [ - "Nicht aktualisieren" - ], - "10 seconds": [ - "10 Sekunden" - ], - "30 seconds": [ - "30 Sekunden" - ], - "1 minute": [ - "1 Minute" - ], - "5 minutes": [ - "5 Minuten" - ], - "Refresh Interval": [ - "Aktualisierung Zwischenzeit" - ], - "Choose the refresh frequency for this dashboard": [ - "Aktualisierungsfrequenz auswählen für dieses Dashboard" - ], - "This dashboard was saved successfully.": [ - "Dashboard wurde erfolgreich gespeichert" - ], - "Sorry, there was an error saving this dashboard: ": [ - "" - ], - "You must pick a name for the new dashboard": [ - "" - ], - "Save Dashboard": [ - "Dashboard speichern" - ], - "Overwrite Dashboard [%s]": [ - "" - ], - "Save as:": [ - "Speichern als:" - ], - "[dashboard name]": [ - "" - ], - "Name": [ - "Name" - ], - "Viz": [ - "" - ], - "Modified": [ - "Geändert" - ], - "Add Slices": [ - "" - ], - "Add a new slice to the dashboard": [ - "" - ], - "Add Slices to Dashboard": [ - "" - ], - "Move chart": [ - "Diagramm bewegen" - ], - "Force refresh data": [ - "Aktualisierung erzwingen" - ], - "Toggle chart description": [ - "Diagramm Beschreibung umschalten" - ], - "Edit chart": [ - "Diagramm bearbeiten" - ], - "Export CSV": [ - "Exportieren als CSV" - ], - "Explore chart": [ - "Diagramm untersuchen" - ], - "Remove chart from dashboard": [ - "Diagramm von Dashboard entfernen" - ], - "%s - untitled": [ - "%s - unbenannt" - ], - "Edit slice properties": [ - "" - ], - "description": [ - "Beschreibung" - ], - "bolt": [ - "" - ], - "Error...": [ - "Fehler..." - ], - "Query": [ - "Abfrage" - ], - "Height": [ - "Höhe" - ], - "Width": [ - "Breite" - ], - "Export to .json": [ - "Exportieren als JSON" - ], - "Export to .csv format": [ - "Exportieren als CSV" - ], - "Please enter a slice name": [ - "Bitten Scheibename eingeben" - ], - "Please select a dashboard": [ - "Bitte Dashboard auswählen" - ], - "Please enter a dashboard name": [ - "Bitte Dashboardname eingeben" - ], - "Save A Slice": [ - "Scheibe speichern" - ], - "Overwrite slice %s": [ - "Überschreiben Scheibe %s" - ], - "Save as": [ - "Speichern als" - ], - "[slice name]": [ - "" - ], - "Do not add to a dashboard": [ - "Nicht zum Dashboard hinzufügen" - ], - "Add slice to existing dashboard": [ - "Schiebe zum vorhandenen Dashboard hinzufügen" - ], - "Add to new dashboard": [ - "Schiebe zum neuen Dashboard hinzufügen" - ], - "Save & go to dashboard": [ - "Speichern & zum Dashboard gehen" - ], - "Check out this slice: %s": [ - "" - ], - "`Min` value should be numeric or empty": [ - "" - ], - "`Max` value should be numeric or empty": [ - "" - ], - "Min": [ - "" - ], - "Max": [ - "" - ], - "Something went wrong while fetching the datasource list": [ - "" - ], - "Click to point to another datasource": [ - "" - ], - "Edit the datasource's configuration": [ - "" - ], - "Select a datasource": [ - "" - ], - "Search / Filter": [ - "" - ], - "Filter value": [ - "" - ], - "Select metric": [ - "" - ], - "Select column": [ - "" - ], - "Select operator": [ - "" - ], - "Add Filter": [ - "" - ], - "Error while fetching data": [ - "" - ], - "Select %s": [ - "" - ], - "textarea": [ - "" - ], - "Edit": [ - "" - ], - "in modal": [ - "" - ], - "Select a visualization type": [ - "Visualisierungstyp wählen" - ], - "Updating chart was stopped": [ - "" - ], - "An error occurred while rendering the visualization: %s": [ - "" - ], - "Perhaps your data has grown, your database is under unusual load, or you are simply querying a data source that is to large to be processed within the timeout range. If that is the case, we recommend that you summarize your data further.": [ - "" - ], - "Network error.": [ - "" - ], - "A reference to the [Time] configuration, taking granularity into account": [ - "" - ], - "Group by": [ - "" - ], - "One or many controls to group by": [ - "" - ], - "Datasource": [ - "Datenquelle" - ], - "Visualization Type": [ - "Visualisierungstyp" - ], - "The type of visualization to display": [ - "Der Visualisierungstyp anzuzeigen" - ], - "Metrics": [ - "Metriken" - ], - "One or many metrics to display": [ - "" - ], - "Y Axis Bounds": [ - "" - ], - "Bounds for the Y axis. When left empty, the bounds are dynamically defined based on the min/max of the data. Note that this feature will only expand the axis range. It won't narrow the data's extent.": [ - "" - ], - "Ordering": [ - "" - ], - "Annotation Layers": [ - "Anmerkungstufe" - ], - "Annotation layers to overlay on the visualization": [ - "Anmerkungstufe auf die Visualisierung zu legen" - ], - "Select a annotation layer": [ - "Anmerkungstufe auswählen" - ], - "Error while fetching annotation layers": [ - "Fehler bei Anmerkungstufeabruf" - ], - "Metric": [ - "Metrik" - ], - "Choose the metric": [ - "Metrik auswählen" - ], - "Right Axis Metric": [ - "" - ], - "Choose a metric for right axis": [ - "" - ], - "Stacked Style": [ - "" - ], - "Linear Color Scheme": [ - "" - ], - "Normalize Across": [ - "" - ], - "Color will be rendered based on a ratio of the cell against the sum of across this criteria": [ - "" - ], - "Horizon Color Scale": [ - "" - ], - "Defines how the color are attributed.": [ - "" - ], - "Rendering": [ - "" - ], - "image-rendering CSS attribute of the canvas object that defines how the browser scales up the image": [ - "" - ], - "XScale Interval": [ - "" - ], - "Number of steps to take between ticks when displaying the X scale": [ - "" - ], - "YScale Interval": [ - "" - ], - "Number of steps to take between ticks when displaying the Y scale": [ - "" - ], - "Include Time": [ - "" - ], - "Whether to include the time granularity as defined in the time section": [ - "" - ], - "Stacked Bars": [ - "" - ], - "Show totals": [ - "" - ], - "Display total row/column": [ - "" - ], - "Show Markers": [ - "" - ], - "Show data points as circle markers on the lines": [ - "" - ], - "Bar Values": [ - "" - ], - "Show the value on top of the bar": [ - "" - ], - "Sort Bars": [ - "" - ], - "Sort bars by x labels.": [ - "" - ], - "Combine Metrics": [ - "" - ], - "Display metrics side by side within each column, as opposed to each column being displayed side by side for each metric.": [ - "" - ], - "Extra Controls": [ - "" - ], - "Whether to show extra controls or not. Extra controls include things like making mulitBar charts stacked or side by side.": [ - "" - ], - "Reduce X ticks": [ - "" - ], - "Reduces the number of X axis ticks to be rendered. If true, the x axis wont overflow and labels may be missing. If false, a minimum width will be applied to columns and the width may overflow into an horizontal scroll.": [ - "" - ], - "Include Series": [ - "" - ], - "Include series name as an axis": [ - "" - ], - "Color Metric": [ - "" - ], - "A metric to use for color": [ - "" - ], - "Country Name": [ - "" - ], - "The name of country that Superset should display": [ - "" - ], - "Country Field Type": [ - "" - ], - "The country code standard that Superset should expect to find in the [country] column": [ - "" - ], - "Columns": [ - "" - ], - "One or many controls to pivot as columns": [ - "" - ], - "Columns to display": [ - "" - ], - "Origin": [ - "" - ], - "Defines the origin where time buckets start, accepts natural dates as in `now`, `sunday` or `1970-01-01`": [ - "" - ], - "Bottom Margin": [ - "" - ], - "Bottom margin, in pixels, allowing for more room for axis labels": [ - "" - ], - "Left Margin": [ - "" - ], - "Left margin, in pixels, allowing for more room for axis labels": [ - "" - ], - "Time Granularity": [ - "" - ], - "The time granularity for the visualization. Note that you can type and use simple natural language as in `10 seconds`, `1 day` or `56 weeks`": [ - "" - ], - "Domain": [ - "" - ], - "The time unit used for the grouping of blocks": [ - "" - ], - "Subdomain": [ - "" - ], - "The time unit for each block. Should be a smaller unit than domain_granularity. Should be larger or equal to Time Grain": [ - "" - ], - "Link Length": [ - "" - ], - "Link length in the force layout": [ - "" - ], - "Charge": [ - "" - ], - "Charge in the force layout": [ - "" - ], - "The time column for the visualization. Note that you can define arbitrary expression that return a DATETIME column in the table or. Also note that the filter below is applied against this column or expression": [ - "" - ], - "Time Grain": [ - "" - ], - "The time granularity for the visualization. This applies a date transformation to alter your time column and defines a new time granularity. The options here are defined on a per database engine basis in the Superset source code.": [ - "" - ], - "Resample Rule": [ - "" - ], - "Pandas resample rule": [ - "" - ], - "Resample How": [ - "" - ], - "Pandas resample how": [ - "" - ], - "Resample Fill Method": [ - "" - ], - "Pandas resample fill method": [ - "" - ], - "Since": [ - "" - ], - "7 days ago": [ - "" - ], - "Until": [ - "" - ], - "Max Bubble Size": [ - "" - ], - "Whisker/outlier options": [ - "" - ], - "Determines how whiskers and outliers are calculated.": [ - "" - ], - "Ratio": [ - "" - ], - "Target aspect ratio for treemap tiles.": [ - "" - ], - "Number format": [ - "" - ], - "Row limit": [ - "" - ], - "Series limit": [ - "" - ], - "Limits the number of time series that get displayed": [ - "" - ], - "Sort By": [ - "" - ], - "Metric used to define the top series": [ - "" - ], - "Rolling": [ - "" - ], - "Defines a rolling window function to apply, works along with the [Periods] text box": [ - "" - ], - "Periods": [ - "" - ], - "Defines the size of the rolling window function, relative to the time granularity selected": [ - "" - ], - "Min Periods": [ - "" - ], - "The minimum number of rolling periods required to show a value. For instance if you do a cumulative sum on 7 days you may want your \"Min Period\" to be 7, so that all data points shown are the total of 7 periods. This will hide the \"ramp up\" taking place over the first 7 periods": [ - "" - ], - "Series": [ - "" - ], - "Defines the grouping of entities. Each series is shown as a specific color on the chart and has a legend toggle": [ - "" - ], - "Entity": [ - "" - ], - "This defines the element to be plotted on the chart": [ - "" - ], - "X Axis": [ - "" - ], - "Metric assigned to the [X] axis": [ - "" - ], - "Y Axis": [ - "" - ], - "Metric assigned to the [Y] axis": [ - "" - ], - "Bubble Size": [ - "" - ], - "URL": [ - "" - ], - "The URL, this control is templated, so you can integrate {{ width }} and/or {{ height }} in your URL string.": [ - "" - ], - "X Axis Label": [ - "" - ], - "Y Axis Label": [ - "" - ], - "Custom WHERE clause": [ - "" - ], - "The text in this box gets included in your query's WHERE clause, as an AND to other criteria. You can include complex expression, parenthesis and anything else supported by the backend it is directed towards.": [ - "" - ], - "Custom HAVING clause": [ - "" - ], - "The text in this box gets included in your query's HAVING clause, as an AND to other criteria. You can include complex expression, parenthesis and anything else supported by the backend it is directed towards.": [ - "" - ], - "Comparison Period Lag": [ - "" - ], - "Based on granularity, number of time periods to compare against": [ - "" - ], - "Comparison suffix": [ - "" - ], - "Suffix to apply after the percentage display": [ - "" - ], - "Table Timestamp Format": [ - "" - ], - "Timestamp Format": [ - "" - ], - "Series Height": [ - "" - ], - "Pixel height of each series": [ - "" - ], - "Page Length": [ - "" - ], - "Rows per page, 0 means no pagination": [ - "" - ], - "X Axis Format": [ - "" - ], - "Y Axis Format": [ - "" - ], - "Right Axis Format": [ - "" - ], - "Markup Type": [ - "" - ], - "Pick your favorite markup language": [ - "" - ], - "Rotation": [ - "" - ], - "Rotation to apply to words in the cloud": [ - "" - ], - "Line Style": [ - "" - ], - "Line interpolation as defined by d3.js": [ - "" - ], - "Label Type": [ - "" - ], - "What should be shown on the label?": [ - "" - ], - "Code": [ - "" - ], - "Put your code here": [ - "" - ], - "Aggregation function": [ - "" - ], - "Aggregate function to apply when pivoting and computing the total rows and columns": [ - "" - ], - "Font Size From": [ - "" - ], - "Font size for the smallest value in the list": [ - "" - ], - "Font Size To": [ - "" - ], - "Font size for the biggest value in the list": [ - "" - ], - "Instant Filtering": [ - "" - ], - "Range Filter": [ - "" - ], - "Whether to display the time range interactive selector": [ - "" - ], - "Date Filter": [ - "" - ], - "Whether to include a time filter": [ - "" - ], - "Data Table": [ - "" - ], - "Whether to display the interactive data table": [ - "" - ], - "Search Box": [ - "" - ], - "Whether to include a client side search box": [ - "" - ], - "Table Filter": [ - "" - ], - "Whether to apply filter when table cell is clicked": [ - "" - ], - "Show Bubbles": [ - "" - ], - "Whether to display bubbles on top of countries": [ - "" - ], - "Legend": [ - "" - ], - "Whether to display the legend (toggles)": [ - "" - ], - "X bounds": [ - "" - ], - "Whether to display the min and max values of the X axis": [ - "" - ], - "Y bounds": [ - "" - ], - "Whether to display the min and max values of the Y axis": [ - "" - ], - "Rich Tooltip": [ - "" - ], - "The rich tooltip shows a list of all series for that point in time": [ - "" - ], - "Y Log Scale": [ - "" - ], - "Use a log scale for the Y axis": [ - "" - ], - "X Log Scale": [ - "" - ], - "Use a log scale for the X axis": [ - "" - ], - "Donut": [ - "" - ], - "Do you want a donut or a pie?": [ - "" - ], - "Put labels outside": [ - "" - ], - "Put the labels outside the pie?": [ - "" - ], - "Contribution": [ - "" - ], - "Compute the contribution to the total": [ - "" - ], - "Period Ratio": [ - "" - ], - "[integer] Number of period to compare against, this is relative to the granularity selected": [ - "" - ], - "Period Ratio Type": [ - "" - ], - "`factor` means (new/previous), `growth` is ((new/previous) - 1), `value` is (new-previous)": [ - "" - ], - "Time Shift": [ - "" - ], - "Overlay a timeseries from a relative time period. Expects relative time delta in natural language (example: 24 hours, 7 days, 56 weeks, 365 days)": [ - "" - ], - "Subheader": [ - "" - ], - "Description text that shows up below your Big Number": [ - "" - ], - "label": [ - "" - ], - "`count` is COUNT(*) if a group by is used. Numerical columns will be aggregated with the aggregator. Non-numerical columns will be used to label points. Leave empty to get a count of points in each cluster.": [ - "" - ], - "Map Style": [ - "" - ], - "Base layer map style": [ - "" - ], - "Clustering Radius": [ - "" - ], - "The radius (in pixels) the algorithm uses to define a cluster. Choose 0 to turn off clustering, but beware that a large number of points (>1000) will cause lag.": [ - "" - ], - "Point Radius": [ - "" - ], - "The radius of individual points (ones that are not in a cluster). Either a numerical column or `Auto`, which scales the point based on the largest cluster": [ - "" - ], - "Point Radius Unit": [ - "" - ], - "The unit of measure for the specified point radius": [ - "" - ], - "Opacity": [ - "" - ], - "Opacity of all clusters, points, and labels. Between 0 and 1.": [ - "" - ], - "Zoom": [ - "" - ], - "Zoom level of the map": [ - "" - ], - "Default latitude": [ - "" - ], - "Latitude of default viewport": [ - "" - ], - "Default longitude": [ - "" - ], - "Longitude of default viewport": [ - "" - ], - "Live render": [ - "" - ], - "Points and clusters will update as viewport is being changed": [ - "" - ], - "RGB Color": [ - "" - ], - "The color for points and clusters in RGB": [ - "" - ], - "Ranges": [ - "" - ], - "Ranges to highlight with shading": [ - "" - ], - "Range labels": [ - "" - ], - "Labels for the ranges": [ - "" - ], - "Markers": [ - "" - ], - "List of values to mark with triangles": [ - "" - ], - "Marker labels": [ - "" - ], - "Labels for the markers": [ - "" - ], - "Marker lines": [ - "" - ], - "List of values to mark with lines": [ - "" - ], - "Marker line labels": [ - "" - ], - "Labels for the marker lines": [ - "" - ], - "Slice ID": [ - "" - ], - "The id of the active slice": [ - "" - ], - "Cache Timeout (seconds)": [ - "" - ], - "The number of seconds before expiring the cache": [ - "" - ], - "Order by entity id": [ - "" - ], - "Important! Select this if the table is not already sorted by entity id, else there is no guarantee that all events for each entity are returned.": [ - "" - ], - "Minimum leaf node event count": [ - "" - ], - "Leaf nodes that represent fewer than this number of events will be initially hidden in the visualization": [ - "" - ], - "Color Scheme": [ - "Farbschema" - ], - "The color scheme for rendering chart": [ - "" - ], - "Time": [ - "" - ], - "Time related form attributes": [ - "" - ], - "Datasource & Chart Type": [ - "" - ], - "This section exposes ways to include snippets of SQL in your query": [ - "" - ], - "Annotations": [ - "Anmerkungen" - ], - "Advanced Analytics": [ - "" - ], - "This section contains options that allow for advanced analytical post processing of query results": [ - "" - ], - "Result Filters": [ - "" - ], - "The filters to apply after post-aggregation.Leave the value control empty to filter empty strings or nulls": [ - "" - ], - "Chart Options": [ - "" - ], - "Breakdowns": [ - "" - ], - "Defines how each series is broken down": [ - "" - ], - "Pie Chart": [ - "" - ], - "Dual Axis Line Chart": [ - "" - ], - "Y Axis 1": [ - "" - ], - "Y Axis 2": [ - "" - ], - "Left Axis Metric": [ - "" - ], - "Choose a metric for left axis": [ - "" - ], - "Left Axis Format": [ - "" - ], - "Axes": [ - "" - ], - "GROUP BY": [ - "" - ], - "Use this section if you want a query that aggregates": [ - "" - ], - "NOT GROUPED BY": [ - "" - ], - "Use this section if you want to query atomic rows": [ - "" - ], - "Options": [ - "" - ], - "Bubbles": [ - "" - ], - "Numeric Column": [ - "" - ], - "Select the numeric column to draw the histogram": [ - "" - ], - "No of Bins": [ - "" - ], - "Select number of bins for the histogram": [ - "" - ], - "Primary Metric": [ - "" - ], - "The primary metric is used to define the arc segment sizes": [ - "" - ], - "Secondary Metric": [ - "" - ], - "This secondary metric is used to define the color as a ratio against the primary metric. If the two metrics match, color is mapped level groups": [ - "" - ], - "Hierarchy": [ - "Hierarchie" - ], - "This defines the level of the hierarchy": [ - "" - ], - "Source / Target": [ - "" - ], - "Choose a source and a target": [ - "" - ], - "Chord Diagram": [ - "" - ], - "Choose a number format": [ - "" - ], - "Source": [ - "" - ], - "Choose a source": [ - "" - ], - "Target": [ - "" - ], - "Choose a target": [ - "" - ], - "ISO 3166-1 codes of region/province/department": [ - "" - ], - "It's ISO 3166-1 of your region/province/department in your table. (see documentation for list of ISO 3166-1)": [ - "" - ], - "Country Control": [ - "" - ], - "3 letter code of the country": [ - "" - ], - "Metric for color": [ - "" - ], - "Metric that defines the color of the country": [ - "" - ], - "Bubble size": [ - "" - ], - "Metric that defines the size of the bubble": [ - "" - ], - "Filter Box": [ - "" - ], - "Filter controls": [ - "" - ], - "The controls you want to filter on. Note that only columns checked as \"filterable\" will show up on this list.": [ - "" - ], - "Heatmap Options": [ - "" - ], - "Horizon": [ - "" - ], - "Points": [ - "" - ], - "Labelling": [ - "" - ], - "Visual Tweaks": [ - "" - ], - "Viewport": [ - "" - ], - "Longitude": [ - "" - ], - "Column containing longitude data": [ - "" - ], - "Latitude": [ - "" - ], - "Column containing latitude data": [ - "" - ], - "Cluster label aggregator": [ - "" - ], - "Aggregate function applied to the list of points in each cluster to produce the cluster label.": [ - "" - ], - "Tooltip": [ - "" - ], - "Show a tooltip when hovering over points and clusters describing the label": [ - "" - ], - "One or many controls to group by. If grouping, latitude and longitude columns must be present.": [ - "" - ], - "Event definition": [ - "" - ], - "Additional meta data": [ - "" - ], - "Column containing entity ids": [ - "" - ], - "e.g., a \"user id\" column": [ - "" - ], - "Column containing event names": [ - "" - ], - "Event count limit": [ - "" - ], - "The maximum number of events to return, equivalent to number of rows": [ - "" - ], - "Meta data": [ - "" - ], - "Select any columns for meta data inspection": [ - "" - ], - "The server could not be reached. You may want to verify your connection and try again.": [ - "" - ], - "An unknown error occurred. (Status: %s )": [ - "" - ], - "Favorites": [ - "Favoriten" - ], - "Created Content": [ - "Erstellte Inhalt" - ], - "Recent Activity": [ - "Kürzliche Aktivitäten" - ], - "Security & Access": [ - "Sicherheit & Zugriff" - ], - "No slices": [ - "Keine Schieben" - ], - "No dashboards": [ - "Keine Dashboards" - ], - "Dashboards": [ - "Dashboards" - ], - "Slices": [ - "Schieben" - ], - "No favorite slices yet, go click on stars!": [ - "Noch keine Lieblingsscheiben, klicken Sie auf ein paar Sterne!" - ], - "No favorite dashboards yet, go click on stars!": [ - "Noch keine Lieblingsdashboards, klicken Sie auf ein paar Sterne!" - ], - "Roles": [ - "Rollen" - ], - "Databases": [ - "Datenbanken" - ], - "Datasources": [ - "Datenquellen" - ], - "Profile picture provided by Gravatar": [ - "Profilbild von Gravatar" - ], - "joined": [ - "" - ], - "id:": [ - "" - ], - "Sorry, there appears to be no data": [ - "" - ], - "Select [%s]": [ - "" - ], - "No data was returned.": [ - "Keine Daten zurückgesendet" - ], - "List Druid Column": [ - "Druid Spalten auflisten" - ], - "Show Druid Column": [ - "Druid Spalte anzeigen" - ], - "Add Druid Column": [ - "Druid Spalte einfügen" - ], - "Edit Druid Column": [ - "Druid Spalte bearbeiten" - ], - "Column": [ - "Spalte" - ], - "Type": [ - "Typ" - ], - "Groupable": [ - "" - ], - "Filterable": [ - "" - ], - "Count Distinct": [ - "" - ], - "Sum": [ - "" - ], - "Whether this column is exposed in the `Filters` section of the explore view.": [ - "" - ], - "List Druid Metric": [ - "Druid Metriken auflisten" - ], - "Show Druid Metric": [ - "Druid Metrik anzeigen" - ], - "Add Druid Metric": [ - "Druid Metrik einfügen" - ], - "Edit Druid Metric": [ - "Druid Metric bearbeiten" - ], - "Whether the access to this metric is restricted to certain roles. Only roles with the permission 'metric access on XXX (the name of this metric)' are allowed to access this metric": [ - "" - ], - "Verbose Name": [ - "" - ], - "JSON": [ - "JSON" - ], - "Druid Datasource": [ - "Druid Datenquelle" - ], - "Warning Message": [ - "Warnmeldung" - ], - "List Druid Cluster": [ - "Druid Cluster auflisten" - ], - "Show Druid Cluster": [ - "Druid Cluster anzeigen" - ], - "Add Druid Cluster": [ - "Druid Cluster einfügen" - ], - "Edit Druid Cluster": [ - "Druid Cluster bearbeiten" - ], - "Cluster": [ - "" - ], - "Broker Host": [ - "" - ], - "Broker Port": [ - "" - ], - "Broker Endpoint": [ - "" - ], - "Druid Clusters": [ - "" - ], - "Sources": [ - "Quellen" - ], - "List Druid Datasource": [ - "Druid Datenquellen auflisten" - ], - "Show Druid Datasource": [ - "Druid Datenquelle anzeigen" - ], - "Add Druid Datasource": [ - "Druid Datenquelle einfügen" - ], - "Edit Druid Datasource": [ - "Druid Datenquelle bearbeiten" - ], - "The list of slices associated with this table. By altering this datasource, you may change how these associated slices behave. Also note that slices need to point to a datasource, so this form will fail at saving if removing slices from a datasource. If you want to change the datasource for a slice, overwrite the slice from the 'explore view'": [ - "" - ], - "Timezone offset (in hours) for this datasource": [ - "" - ], - "Time expression to use as a predicate when retrieving distinct values to populate the filter component. Only applies when `Enable Filter Select` is on. If you enter `7 days ago`, the distinct list of values in the filter will be populated based on the distinct value over the past week": [ - "" - ], - "Whether to populate the filter's dropdown in the explore view's filter section with a list of distinct values fetched from the backend on the fly": [ - "" - ], - "Redirects to this endpoint when clicking on the datasource from the datasource list": [ - "" - ], - "Associated Slices": [ - "" - ], - "Data Source": [ - "Datenquelle" - ], - "Owner": [ - "" - ], - "Is Hidden": [ - "" - ], - "Enable Filter Select": [ - "" - ], - "Default Endpoint": [ - "" - ], - "Time Offset": [ - "" - ], - "Cache Timeout": [ - "" - ], - "Druid Datasources": [ - "Druid Datenquellen" - ], - "Scan New Datasources": [ - "Neue Datenquellen suchen" - ], - "Refresh Druid Metadata": [ - "Druid Metadata aktualisieren" - ], - "Datetime column not provided as part table configuration and is required by this type of chart": [ - "" - ], - "Empty query?": [ - "Leere Abfrage?" - ], - "Metric '{}' is not valid": [ - "Metrik '{}' ist nicht valide" - ], - "Table [{}] doesn't seem to exist in the specified database, couldn't fetch column information": [ - "" - ], - "List Columns": [ - "Spalten auflisten" - ], - "Show Column": [ - "Spalte anzeigen" - ], - "Add Column": [ - "Spalte einfügen" - ], - "Edit Column": [ - "Spalte bearbeiten" - ], - "Whether to make this column available as a [Time Granularity] option, column has to be DATETIME or DATETIME-like": [ - "" - ], - "The data type that was inferred by the database. It may be necessary to input a type manually for expression-defined columns in some cases. In most case users should not need to alter this.": [ - "" - ], - "Expression": [ - "" - ], - "Is temporal": [ - "" - ], - "Datetime Format": [ - "" - ], - "Database Expression": [ - "" - ], - "List Metrics": [ - "Metriken auflisten" - ], - "Show Metric": [ - "Metrik anzeigen" - ], - "Add Metric": [ - "Metrik einfügen" - ], - "Edit Metric": [ - "Metrik bearbeiten" - ], - "SQL Expression": [ - "" - ], - "D3 Format": [ - "" - ], - "Is Restricted": [ - "Ist begrenzt" - ], - "List Tables": [ - "Tabellen auflisten" - ], - "Show Table": [ - "Tabelle anzeigen" - ], - "Add Table": [ - "Tabelle einfügen" - ], - "Edit Table": [ - "Tabelle bearbeiten" - ], - "Name of the table that exists in the source database": [ - "" - ], - "Schema, as used only in some databases like Postgres, Redshift and DB2": [ - "" - ], - "This fields acts a Superset view, meaning that Superset will run a query against this string as a subquery.": [ - "" - ], - "Predicate applied when fetching distinct value to populate the filter control component. Supports jinja template syntax. Applies only when `Enable Filter Select` is on.": [ - "" - ], - "Redirects to this endpoint when clicking on the table from the table list": [ - "" - ], - "Changed By": [ - "Bearbeitet von" - ], - "Database": [ - "Datenbank" - ], - "Last Changed": [ - "Bearbeitet am" - ], - "Schema": [ - "" - ], - "Offset": [ - "" - ], - "Table Name": [ - "Tabellenname" - ], - "Fetch Values Predicate": [ - "" - ], - "Main Datetime Column": [ - "" - ], - "Table [{}] could not be found, please double check your database connection, schema, and table name": [ - "" - ], - "The table was created. As part of this two phase configuration process, you should now click the edit button by the new table to configure it.": [ - "" - ], - "Tables": [ - "Tabellen" - ], - "Profile": [ - "Profil" - ], - "Logout": [ - "Abmelden" - ], - "Login": [ - "Anmelden" - ], - "Record Count": [ - "Datensätzeanzahl" - ], - "No records found": [ - "Keine Datensätze gefunden" - ], - "Import": [ - "Importieren" - ], - "No Access!": [ - "Keine Zugriff!" - ], - "You do not have permissions to access the datasource(s): %(name)s.": [ - "" - ], - "Request Permissions": [ - "Berechtigung anfordern" - ], - "Welcome!": [ - "Willkommen!" - ], - "Test Connection": [ - "Verbindungstest" - ], - "Manage": [ - "Einstellungen" - ], - "Datasource %(name)s already exists": [ - "" - ], - "json isn't valid": [ - "" - ], - "Delete": [ - "Löschen" - ], - "Delete all Really?": [ - "Wirklich alle löschen?" - ], - "This endpoint requires the `all_datasource_access` permission": [ - "" - ], - "The datasource seems to have been deleted": [ - "" - ], - "The access requests seem to have been deleted": [ - "" - ], - "The user seems to have been deleted": [ - "" - ], - "You don't have access to this datasource": [ - "Sie haben keine Zugriff auf diese Datenquelle" - ], - "This view requires the database %(name)s or `all_datasource_access` permission": [ - "" - ], - "This endpoint requires the datasource %(name)s, database or `all_datasource_access` permission": [ - "" - ], - "List Databases": [ - "Dakenbanken auflisten" - ], - "Show Database": [ - "Datenbank anzeigen" - ], - "Add Database": [ - "Datenbank einfügen" - ], - "Edit Database": [ - "Datenbank bearbeiten" - ], - "Expose this DB in SQL Lab": [ - "" - ], - "Allow users to run synchronous queries, this is the default and should work well for queries that can be executed within a web request scope (<~1 minute)": [ - "" - ], - "Allow users to run queries, against an async backend. This assumes that you have a Celery worker setup as well as a results backend.": [ - "" - ], - "Allow CREATE TABLE AS option in SQL Lab": [ - "" - ], - "Allow users to run non-SELECT statements (UPDATE, DELETE, CREATE, ...) in SQL Lab": [ - "" - ], - "When allowing CREATE TABLE AS option in SQL Lab, this option forces the table to be created in this schema": [ - "" - ], - "All the queries in Sql Lab are going to be executed on behalf of currently authorized user.": [ - "" - ], - "Expose in SQL Lab": [ - "" - ], - "Allow CREATE TABLE AS": [ - "" - ], - "Allow DML": [ - "" - ], - "CTAS Schema": [ - "" - ], - "Creator": [ - "Schöpfer" - ], - "SQLAlchemy URI": [ - "" - ], - "Extra": [ - "" - ], - "Allow Run Sync": [ - "" - ], - "Allow Run Async": [ - "" - ], - "Impersonate queries to the database": [ - "" - ], - "Import Dashboards": [ - "Dashboards importieren" - ], - "User": [ - "Benutzer" - ], - "User Roles": [ - "Benutzer Rollen" - ], - "Database URL": [ - "Datenbank URL" - ], - "Roles to grant": [ - "" - ], - "Created On": [ - "" - ], - "Access requests": [ - "Zugriffsanforderungen" - ], - "Security": [ - "Sicherheit" - ], - "List Slices": [ - "Schieben auflisten" - ], - "Show Slice": [ - "Schiebe anzeigen" - ], - "Add Slice": [ - "Schiebe einfügen" - ], - "Edit Slice": [ - "Schiebe bearbeiten" - ], - "These parameters are generated dynamically when clicking the save or overwrite button in the explore view. This JSON object is exposed here for reference and for power users who may want to alter specific parameters.": [ - "" - ], - "Duration (in seconds) of the caching timeout for this slice.": [ - "" - ], - "Last Modified": [ - "Geändert" - ], - "Owners": [ - "" - ], - "Parameters": [ - "Parameter" - ], - "Slice": [ - "Schiebe" - ], - "List Dashboards": [ - "Dashboards auflisten" - ], - "Show Dashboard": [ - "Dashboard anzeigen" - ], - "Add Dashboard": [ - "Dashboard einfügen" - ], - "Edit Dashboard": [ - "Dashboard bearbeiten" - ], - "This json object describes the positioning of the widgets in the dashboard. It is dynamically generated when adjusting the widgets size and positions by using drag & drop in the dashboard view": [ - "" - ], - "The css for individual dashboards can be altered here, or in the dashboard view where changes are immediately visible": [ - "" - ], - "To get a readable URL for your dashboard": [ - "" - ], - "This JSON object is generated dynamically when clicking the save or overwrite button in the dashboard view. It is exposed here for reference and for power users who may want to alter specific parameters.": [ - "" - ], - "Owners is a list of users who can alter the dashboard.": [ - "" - ], - "Dashboard": [ - "" - ], - "Slug": [ - "" - ], - "Position JSON": [ - "" - ], - "JSON Metadata": [ - "" - ], - "Underlying Tables": [ - "" - ], - "Export": [ - "" - ], - "Export dashboards?": [ - "" - ], - "Action": [ - "Aktion" - ], - "dttm": [ - "" - ], - "Action Log": [ - "Aktionsprotokoll" - ], - "Access was requested": [ - "Zugang wurde beantragt" - ], - "%(user)s was granted the role %(role)s that gives access to the %(datasource)s": [ - "" - ], - "Role %(r)s was extended to provide the access to the datasource %(ds)s": [ - "" - ], - "You have no permission to approve this request": [ - "" - ], - "Malformed request. slice_id or table_name and db_name arguments are expected": [ - "" - ], - "Slice %(id)s not found": [ - "" - ], - "Table %(t)s wasn't found in the database %(d)s": [ - "" - ], - "Can't find User '%(name)s', please ask your admin to create one.": [ - "" - ], - "Can't find DruidCluster with cluster_name = '%(name)s'": [ - "" - ], - "Query record was not created as expected.": [ - "" - ], - "Template Name": [ - "Vorlagename" - ], - "CSS Templates": [ - "CSS Vorlagen" - ], - "SQL Editor": [ - "" - ], - "SQL Lab": [ - "" - ], - "Query Search": [ - "Abfragen suchen" - ], - "Status": [ - "" - ], - "Start Time": [ - "Von Zeit" - ], - "End Time": [ - "Bis Zeit" - ], - "Queries": [ - "Abfragen" - ], - "List Saved Query": [ - "Gespeicherte Abfragen auflisten" - ], - "Show Saved Query": [ - "Gespeicherte Abfrage anzeigen" - ], - "Add Saved Query": [ - "Gespeicherte Abfrage einfügen" - ], - "Edit Saved Query": [ - "Gespeicherte Abfrage bearbeiten" - ], - "Pop Tab Link": [ - "" - ], - "Changed on": [ - "Bearbeitet am" - ], - "Saved Queries": [ - "Gespeicherte Abfragen" - ] - } - } -} \ No newline at end of file +{"domain":"superset","locale_data":{"superset":{"":{"domain":"superset","plural_forms":"nplurals=2; plural=(n != 1)","lang":"de"},"Time Column":["Zeitspalte"],"second":["Sekunde"],"minute":["Minute"],"hour":["Stunde"],"day":["Tag"],"week":["Woche"],"month":["Monat"],"quarter":["Vierteljahr"],"year":["Jahr"],"week_start_monday":[""],"week_ending_saturday":[""],"week_start_sunday":[""],"5 minute":["5 Minuten"],"half hour":["halbe Stunde"],"10 minute":["10 Minuten"],"[Superset] Access to the datasource %(name)s was granted":["[Superset] Zugriff auf Datenquelle %(name)s war genehmigt "],"Viz fehlt eine Datenquelle":[""],"From date cannot be larger than to date":["'Von Datum' kann nicht größer als 'bis Datum' sein"],"Table View":["Tabellenansicht"],"Pick a granularity in the Time section or uncheck 'Include Time'":[""],"Choose either fields to [Group By] and [Metrics] or [Columns], not both":[""],"Pivot Table":["Pivot-Tabelle"],"Please choose at least one \"Group by\" field ":[""],"Please choose at least one metric":["Bitte wählt zumindest eine Metrik"],"'Group By' and 'Columns' can't overlap":[""],"Markup":["Auszeichnung"],"Separator":[""],"Word Cloud":["Wortwolke"],"Treemap":[""],"Calendar Heatmap":[""],"Box Plot":["Box-Plot"],"Bubble Chart":[""],"Pick a metric for x, y and size":[""],"Bullet Chart":[""],"Pick a metric to display":[""],"Big Number with Trendline":[""],"Pick a metric!":["Wählt eine Metrik!"],"Big Number":["Große Nummer"],"Time Series - Line Chart":[""],"Pick a time granularity for your time series":[""],"Time Series - Dual Axis Line Chart":[""],"Pick a metric for left axis!":[""],"Pick a metric for right axis!":[""],"Please choose different metrics on left and right axis":[""],"Time Series - Bar Chart":[""],"Time Series - Percent Change":[""],"Time Series - Stacked":[""],"Distribution - NVD3 - Pie Chart":[""],"Histogram":["Histogramm"],"Must have one numeric column specified":[""],"Distribution - Bar Chart":[""],"Can't have overlap between Series and Breakdowns":[""],"Pick at least one metric":[""],"Pick at least one field for [Series]":[""],"Sunburst":[""],"Sankey":[""],"Pick exactly 2 columns as [Source / Target]":[""],"There's a loop in your Sankey, please provide a tree. Here's a faulty link: {}":[""],"Directed Force Layout":[""],"Pick exactly 2 columns to 'Group By'":[""],"Country Map":[""],"World Map":[""],"Filters":[""],"Pick at least one filter field":[""],"iFrame":[""],"Parallel Coordinates":[""],"Heatmap":[""],"Horizon Charts":[""],"Mapbox":[""],"Must have a [Group By] column to have 'count' as the [Label]":[""],"Choice of [Label] must be present in [Group By]":[""],"Choice of [Point Radius] must be present in [Group By]":[""],"[Longitude] and [Latitude] columns must be present in [Group By]":[""],"Event flow":[""],"Time Series - Paired t-test":[""],"Your query was saved":["Ihre Abfrage wurde gespeichert"],"Your query could not be saved":["Ihre Abfrage konnte nicht gespeichert werden"],"Failed at retrieving results from the results backend":[""],"Could not connect to server":[""],"Your session timed out, please refresh your page and try again.":[""],"Query was stopped.":[""],"Failed at stopping query.":[""],"Error occurred while fetching table metadata":[""],"shared query":[""],"The query couldn't be loaded":[""],"An error occurred while creating the data source":[""],"Pick a chart type!":[""],"To use this chart type you need at least one column flagged as a date":[""],"To use this chart type you need at least one dimension":[""],"To use this chart type you need at least one aggregation function":[""],"Untitled Query":[""],"Copy of %s":[""],"share query":[""],"copy URL to clipboard":[""],"Raw SQL":[""],"Source SQL":[""],"SQL":[""],"No query history yet...":[""],"It seems you don't have access to any database":[""],"Search Results":[""],"[From]-":[""],"[To]-":[""],"[Query Status]":[""],"Search":["Suche"],"Open in SQL Editor":["Bearbeiten in SQL Editor"],"view results":[""],"Data preview":[""],"Visualize the data out of this query":[""],"Overwrite text in editor with a query on this table":[""],"Run query in a new tab":[""],"Remove query from log":[""],".CSV":[""],"Visualize":["Visualalisieren"],"Table":["Tabellen"],"was created":[""],"Query in a new tab":[""],"Fetch data preview":[""],"Track Job":[""],"Loading...":[""],"Run Selected Query":[""],"Run Query":[""],"Run query asynchronously":[""],"Stop":["Stopp"],"Undefined":[""],"Label":[""],"Label for your query":[""],"Description":[""],"Write a description for your query":[""],"Save":[""],"Cancel":[""],"Save Query":[""],"Run a query to display results here":[""],"Preview for %s":[""],"Results":[""],"Query History":[""],"Create table as with query results":[""],"new table name":[""],"Error while fetching table list":[""],"Error while fetching schema list":[""],"Error while fetching database list":[""],"Database:":[""],"Select a database":[""],"Select a schema (%s)":[""],"Schema:":[""],"Add a table (%s)":[""],"Type to search ...":[""],"Reset State":[""],"Enter a new title for the tab":[""],"Untitled Query %s":[""],"close tab":[""],"rename tab":[""],"expand tool bar":[""],"hide tool bar":[""],"Copy partition query to clipboard":[""],"latest partition:":[""],"Keys for table":[""],"View keys & indexes (%s)":[""],"Sort columns alphabetically":[""],"Original table column order":[""],"Copy SELECT statement to clipboard":[""],"Remove table preview":[""],"%s is not right as a column name, please alias it (as in SELECT count(*) ":[""],"AS my_alias":[""],"using only alphanumeric characters and underscores":[""],"Creating a data source and popping a new tab":[""],"No results available for this query":[""],"Chart Type":[""],"[Chart Type]":[""],"Datasource Name":[""],"datasource name":[""],"Select ...":[""],"Loaded data cached":[""],"Loaded from cache":[""],"Click to force-refresh":[""],"Copy to clipboard":[""],"Not successful":[""],"Sorry, your browser does not support copying. Use Ctrl / Cmd + C!":[""],"Copied!":[""],"Title":["Titel"],"click to edit title":[""],"You don't have the rights to alter this title.":[""],"Click to favorite/unfavorite":[""],"You have unsaved changes.":[""],"Click the":[""],"button on the top right to save your changes.":[""],"Served from data cached %s . Click to force refresh.":["Von Daten dienten um %s im Cache gespeichert. Aktualisierung erzwingen durch Klicken"],"Click to force refresh":["Aktualisierung erzwingen"],"Error":["Fehler"],"Sorry, there was an error adding slices to this dashboard: %s":[""],"Active Dashboard Filters":[""],"Checkout this dashboard: %s":[""],"Force refresh the whole dashboard":["Ganze Dashboard Aktualisierung erzwingen"],"Edit this dashboard's properties":["Dashboardeigenschaften bearbeiten"],"Load a template":["Vorlage laden"],"Load a CSS template":["CSS Vorlage laden"],"CSS":["CSS"],"Live CSS Editor":[""],"Don't refresh":["Nicht aktualisieren"],"10 seconds":["10 Sekunden"],"30 seconds":["30 Sekunden"],"1 minute":["1 Minute"],"5 minutes":["5 Minuten"],"Refresh Interval":["Aktualisierung Zwischenzeit"],"Choose the refresh frequency for this dashboard":["Aktualisierungsfrequenz auswählen für dieses Dashboard"],"This dashboard was saved successfully.":["Dashboard wurde erfolgreich gespeichert"],"Sorry, there was an error saving this dashboard: ":[""],"You must pick a name for the new dashboard":[""],"Save Dashboard":["Dashboard speichern"],"Overwrite Dashboard [%s]":[""],"Save as:":["Speichern als:"],"[dashboard name]":[""],"Name":["Name"],"Viz":[""],"Modified":["Geändert"],"Add Slices":[""],"Add a new slice to the dashboard":[""],"Add Slices to Dashboard":[""],"Move chart":["Diagramm bewegen"],"Force refresh data":["Aktualisierung erzwingen"],"Toggle chart description":["Diagramm Beschreibung umschalten"],"Edit chart":["Diagramm bearbeiten"],"Export CSV":["Exportieren als CSV"],"Explore chart":["Diagramm untersuchen"],"Remove chart from dashboard":["Diagramm von Dashboard entfernen"],"%s - untitled":["%s - unbenannt"],"Edit slice properties":[""],"description":["Beschreibung"],"bolt":[""],"Error...":["Fehler..."],"Query":["Abfrage"],"Height":["Höhe"],"Width":["Breite"],"Export to .json":["Exportieren als JSON"],"Export to .csv format":["Exportieren als CSV"],"Please enter a slice name":["Bitten Scheibename eingeben"],"Please select a dashboard":["Bitte Dashboard auswählen"],"Please enter a dashboard name":["Bitte Dashboardname eingeben"],"Save A Slice":["Scheibe speichern"],"Overwrite slice %s":["Überschreiben Scheibe %s"],"Save as":["Speichern als"],"[slice name]":[""],"Do not add to a dashboard":["Nicht zum Dashboard hinzufügen"],"Add slice to existing dashboard":["Schiebe zum vorhandenen Dashboard hinzufügen"],"Add + to new dashboard":["Schiebe zum neuen Dashboard hinzufügen"],"Save & go to dashboard":["Speichern & zum Dashboard gehen"],"Check out this slice: %s":[""],"`Min` value should be numeric or empty":[""],"`Max` value should be numeric or empty":[""],"Min":[""],"Max":[""],"Something went wrong while fetching the datasource list":[""],"Click to point to another datasource":[""],"Edit the datasource's configuration":[""],"Select a datasource":[""],"Search / Filter":[""],"Filter value":[""],"Select metric":[""],"Select column":[""],"Select operator":[""],"Add Filter":[""],"Error while fetching data":[""],"Select %s":[""],"textarea":[""],"Edit":[""],"in modal":[""],"Select a visualization type":["Visualisierungstyp wählen"],"Updating chart was stopped":[""],"An error occurred while rendering the visualization: %s":[""],"Perhaps your data has grown, your database is under unusual load, or you are simply querying a data source that is to large to be processed within the timeout range. If that is the case, we recommend that you summarize your data further.":[""],"Network error.":[""],"A reference to the [Time] configuration, taking granularity into account":[""],"Group by":[""],"One or many controls to group by":[""],"Datasource":["Datenquelle"],"Visualization Type":["Visualisierungstyp"],"The type of visualization to display":["Der Visualisierungstyp anzuzeigen"],"Metrics":["Metriken"],"One or many metrics to display":[""],"Y Axis Bounds":[""],"Bounds for the Y axis. When left empty, the bounds are dynamically defined based on the min/max of the data. Note that this feature will only expand the axis range. It won't narrow the data's extent.":[""],"Ordering":[""],"Annotation Layers":["Anmerkungstufe"],"Annotation layers to overlay on the visualization":["Anmerkungstufe auf die Visualisierung zu legen"],"Select a annotation layer":["Anmerkungstufe auswählen"],"Error while fetching annotation layers":["Fehler bei Anmerkungstufeabruf"],"Metric":["Metrik"],"Choose the metric":["Metrik auswählen"],"Right Axis Metric":[""],"Choose a metric for right axis":[""],"Stacked Style":[""],"Linear Color Scheme":[""],"Normalize Across":[""],"Color will be rendered based on a ratio of the cell against the sum of across this criteria":[""],"Horizon Color Scale":[""],"Defines how the color are attributed.":[""],"Rendering":[""],"image-rendering CSS attribute of the canvas object that defines how the browser scales up the image":[""],"XScale Interval":[""],"Number of steps to take between ticks when displaying the X scale":[""],"YScale Interval":[""],"Number of steps to take between ticks when displaying the Y scale":[""],"Include Time":[""],"Whether to include the time granularity as defined in the time section":[""],"Stacked Bars":[""],"Show totals":[""],"Display total row/column":[""],"Show Markers":[""],"Show data points as circle markers on the lines":[""],"Bar Values":[""],"Show the value on top of the bar":[""],"Sort Bars":[""],"Sort bars by x labels.":[""],"Combine Metrics":[""],"Display metrics side by side within each column, as opposed to each column being displayed side by side for each metric.":[""],"Extra Controls":[""],"Whether to show extra controls or not. Extra controls include things like making mulitBar charts stacked or side by side.":[""],"Reduce X ticks":[""],"Reduces the number of X axis ticks to be rendered. If true, the x axis wont overflow and labels may be missing. If false, a minimum width will be applied to columns and the width may overflow into an horizontal scroll.":[""],"Include Series":[""],"Include series name as an axis":[""],"Color Metric":[""],"A metric to use for color":[""],"Country Name":[""],"The name of country that Superset should display":[""],"Country Field Type":[""],"The country code standard that Superset should expect to find in the [country] column":[""],"Columns":[""],"One or many controls to pivot as columns":[""],"Columns to display":[""],"Origin":[""],"Defines the origin where time buckets start, accepts natural dates as in `now`, `sunday` or `1970-01-01`":[""],"Bottom Margin":[""],"Bottom margin, in pixels, allowing for more room for axis labels":[""],"Left Margin":[""],"Left margin, in pixels, allowing for more room for axis labels":[""],"Time Granularity":[""],"The time granularity for the visualization. Note that you can type and use simple natural language as in `10 seconds`, `1 day` or `56 weeks`":[""],"Domain":[""],"The time unit used for the grouping of blocks":[""],"Subdomain":[""],"The time unit for each block. Should be a smaller unit than domain_granularity. Should be larger or equal to Time Grain":[""],"Link Length":[""],"Link length in the force layout":[""],"Charge":[""],"Charge in the force layout":[""],"The time column for the visualization. Note that you can define arbitrary expression that return a DATETIME column in the table or. Also note that the filter below is applied against this column or expression":[""],"Time Grain":[""],"The time granularity for the visualization. This applies a date transformation to alter your time column and defines a new time granularity. The options here are defined on a per database engine basis in the Superset source code.":[""],"Resample Rule":[""],"Pandas resample rule":[""],"Resample How":[""],"Pandas resample how":[""],"Resample Fill Method":[""],"Pandas resample fill method":[""],"Since":[""],"7 days ago":[""],"Until":[""],"Max Bubble Size":[""],"Whisker/outlier options":[""],"Determines how whiskers and outliers are calculated.":[""],"Ratio":[""],"Target aspect ratio for treemap tiles.":[""],"Number format":[""],"Row limit":[""],"Series limit":[""],"Limits the number of time series that get displayed":[""],"Sort By":[""],"Metric used to define the top series":[""],"Rolling":[""],"Defines a rolling window function to apply, works along with the [Periods] text box":[""],"Periods":[""],"Defines the size of the rolling window function, relative to the time granularity selected":[""],"Min Periods":[""],"The minimum number of rolling periods required to show a value. For instance if you do a cumulative sum on 7 days you may want your \"Min Period\" to be 7, so that all data points shown are the total of 7 periods. This will hide the \"ramp up\" taking place over the first 7 periods":[""],"Series":[""],"Defines the grouping of entities. Each series is shown as a specific color on the chart and has a legend toggle":[""],"Entity":[""],"This defines the element to be plotted on the chart":[""],"X Axis":[""],"Metric assigned to the [X] axis":[""],"Y Axis":[""],"Metric assigned to the [Y] axis":[""],"Bubble Size":[""],"URL":[""],"The URL, this control is templated, so you can integrate {{ width }} and/or {{ height }} in your URL string.":[""],"X Axis Label":[""],"Y Axis Label":[""],"Custom WHERE clause":[""],"The text in this box gets included in your query's WHERE clause, as an AND to other criteria. You can include complex expression, parenthesis and anything else supported by the backend it is directed towards.":[""],"Custom HAVING clause":[""],"The text in this box gets included in your query's HAVING clause, as an AND to other criteria. You can include complex expression, parenthesis and anything else supported by the backend it is directed towards.":[""],"Comparison Period Lag":[""],"Based on granularity, number of time periods to compare against":[""],"Comparison suffix":[""],"Suffix to apply after the percentage display":[""],"Table Timestamp Format":[""],"Timestamp Format":[""],"Series Height":[""],"Pixel height of each series":[""],"Page Length":[""],"Rows per page, 0 means no pagination":[""],"X Axis Format":[""],"Y Axis Format":[""],"Right Axis Format":[""],"Markup Type":[""],"Pick your favorite markup language":[""],"Rotation":[""],"Rotation to apply to words in the cloud":[""],"Line Style":[""],"Line interpolation as defined by d3.js":[""],"Label Type":[""],"What should be shown on the label?":[""],"Code":[""],"Put your code here":[""],"Aggregation function":[""],"Aggregate function to apply when pivoting and computing the total rows and columns":[""],"Font Size From":[""],"Font size for the smallest value in the list":[""],"Font Size To":[""],"Font size for the biggest value in the list":[""],"Instant Filtering":[""],"Range Filter":[""],"Whether to display the time range interactive selector":[""],"Date Filter":[""],"Whether to include a time filter":[""],"Data Table":[""],"Whether to display the interactive data table":[""],"Search Box":[""],"Whether to include a client side search box":[""],"Table Filter":[""],"Whether to apply filter when table cell is clicked":[""],"Show Bubbles":[""],"Whether to display bubbles on top of countries":[""],"Legend":[""],"Whether to display the legend (toggles)":[""],"X bounds":[""],"Whether to display the min and max values of the X axis":[""],"Y bounds":[""],"Whether to display the min and max values of the Y axis":[""],"Rich Tooltip":[""],"The + rich tooltip shows a list of all series for that point in time":[""],"Y Log Scale":[""],"Use a log scale for the Y axis":[""],"X Log Scale":[""],"Use a log scale for the X axis":[""],"Donut":[""],"Do you want a donut or a pie?":[""],"Put labels outside":[""],"Put the labels outside the pie?":[""],"Contribution":[""],"Compute the contribution to the total":[""],"Period Ratio":[""],"[integer] Number of period to compare against, this is relative to the granularity selected":[""],"Period Ratio Type":[""],"`factor` means (new/previous), `growth` is ((new/previous) - 1), `value` is (new-previous)":[""],"Time Shift":[""],"Overlay a timeseries from a relative time period. Expects relative time delta in natural language (example: 24 hours, 7 days, 56 weeks, 365 days)":[""],"Subheader":[""],"Description text that shows up below your Big Number":[""],"label":[""],"`count` is COUNT(*) if a group by is used. Numerical columns will be aggregated with the aggregator. Non-numerical columns will be used to label points. Leave empty to get a count of points in each cluster.":[""],"Map Style":[""],"Base layer map style":[""],"Clustering Radius":[""],"The radius (in pixels) the algorithm uses to define a cluster. Choose 0 to turn off clustering, but beware that a large number of points (>1000) will cause lag.":[""],"Point Radius":[""],"The radius of individual points (ones that are not in a cluster). Either a numerical column or `Auto`, which scales the point based on the largest cluster":[""],"Point Radius Unit":[""],"The unit of measure for the specified point radius":[""],"Opacity":[""],"Opacity of all clusters, points, and labels. Between 0 and 1.":[""],"Zoom":[""],"Zoom level of the map":[""],"Default latitude":[""],"Latitude of default viewport":[""],"Default longitude":[""],"Longitude of default viewport":[""],"Live render":[""],"Points and clusters will update as viewport is being changed":[""],"RGB Color":[""],"The color for points and clusters in RGB":[""],"Ranges":[""],"Ranges to highlight with shading":[""],"Range labels":[""],"Labels for the ranges":[""],"Markers":[""],"List of values to mark with triangles":[""],"Marker labels":[""],"Labels for the markers":[""],"Marker lines":[""],"List of values to mark with lines":[""],"Marker line labels":[""],"Labels for the marker lines":[""],"Slice ID":[""],"The id of the active slice":[""],"Cache Timeout (seconds)":[""],"The number of seconds before expiring the cache":[""],"Order by entity id":[""],"Important! Select this if the table is not already sorted by entity id, else there is no guarantee that all events for each entity are returned.":[""],"Minimum leaf node event count":[""],"Leaf nodes that represent fewer than this number of events will be initially hidden in the visualization":[""],"Color Scheme":["Farbschema"],"The color scheme for rendering chart":[""],"Time":[""],"Time related form attributes":[""],"Datasource & Chart Type":[""],"This section exposes ways to include snippets of SQL in your query":[""],"Annotations":["Anmerkungen"],"Advanced Analytics":[""],"This section contains options that allow for advanced analytical post processing of query results":[""],"Result Filters":[""],"The filters to apply after post-aggregation.Leave the value control empty to filter empty strings or nulls":[""],"Chart Options":[""],"Breakdowns":[""],"Defines how each series is broken down":[""],"Pie Chart":[""],"Dual Axis Line Chart":[""],"Y Axis 1":[""],"Y Axis 2":[""],"Left Axis Metric":[""],"Choose a metric for left axis":[""],"Left Axis Format":[""],"Axes":[""],"GROUP BY":[""],"Use this section if you want a query that aggregates":[""],"NOT GROUPED BY":[""],"Use this section if you want to query atomic rows":[""],"Options":[""],"Bubbles":[""],"Numeric Column":[""],"Select the numeric column to draw the histogram":[""],"No of Bins":[""],"Select number of bins for the histogram":[""],"Primary Metric":[""],"The primary metric is used to define the arc segment sizes":[""],"Secondary Metric":[""],"This secondary metric is used to define the color as a ratio against the primary metric. If the two metrics match, color is mapped level groups":[""],"Hierarchy":["Hierarchie"],"This defines the level of the hierarchy":[""],"Source / Target":[""],"Choose a source and a target":[""],"Chord Diagram":[""],"Choose a number format":[""],"Source":[""],"Choose a source":[""],"Target":[""],"Choose a target":[""],"ISO 3166-1 codes of region/province/department":[""],"It's ISO 3166-1 of your region/province/department in your table. (see documentation for list of ISO 3166-1)":[""],"Country Control":[""],"3 letter code of the country":[""],"Metric for color":[""],"Metric that defines the color of the country":[""],"Bubble size":[""],"Metric that defines the size of the bubble":[""],"Filter Box":[""],"Filter controls":[""],"The controls you want to filter on. Note that only columns checked as \"filterable\" will show up on this list.":[""],"Heatmap Options":[""],"Horizon":[""],"Points":[""],"Labelling":[""],"Visual Tweaks":[""],"Viewport":[""],"Longitude":[""],"Column containing longitude data":[""],"Latitude":[""],"Column containing latitude data":[""],"Cluster label aggregator":[""],"Aggregate function applied to the list of points in each cluster to produce the cluster label.":[""],"Tooltip":[""],"Show a tooltip when hovering over points and clusters describing the label":[""],"One or many controls to group by. If grouping, latitude and longitude columns must be present.":[""],"Event definition":[""],"Additional meta data":[""],"Column containing entity ids":[""],"e.g., a \"user id\" column":[""],"Column containing event names":[""],"Event count limit":[""],"The maximum number of events to return, equivalent to number of rows":[""],"Meta data":[""],"Select any columns for meta data inspection":[""],"The server could not be reached. You may want to verify your connection and try again.":[""],"An unknown error occurred. (Status: %s )":[""],"Favorites":["Favoriten"],"Created Content":["Erstellte Inhalt"],"Recent Activity":["Kürzliche Aktivitäten"],"Security & Access":["Sicherheit & Zugriff"],"No slices":["Keine Schieben"],"No dashboards":["Keine Dashboards"],"Dashboards":["Dashboards"],"Slices":["Schieben"],"No favorite slices yet, go click on stars!":["Noch keine Lieblingsscheiben, klicken Sie auf ein paar Sterne!"],"No favorite dashboards yet, go click on stars!":["Noch keine Lieblingsdashboards, klicken Sie auf ein paar Sterne!"],"Roles":["Rollen"],"Databases":["Datenbanken"],"Datasources":["Datenquellen"],"Profile picture provided by Gravatar":["Profilbild von Gravatar"],"joined":[""],"id:":[""],"Sorry, there appears to be no data":[""],"Select [%s]":[""],"No data was returned.":["Keine Daten zurückgesendet"],"List Druid Column":["Druid Spalten auflisten"],"Show Druid Column":["Druid Spalte anzeigen"],"Add Druid Column":["Druid Spalte einfügen"],"Edit Druid Column":["Druid Spalte bearbeiten"],"Column":["Spalte"],"Type":["Typ"],"Groupable":[""],"Filterable":[""],"Count Distinct":[""],"Sum":[""],"Whether this column is exposed in the `Filters` section of the explore view.":[""],"List Druid Metric":["Druid Metriken auflisten"],"Show Druid Metric":["Druid Metrik anzeigen"],"Add Druid Metric":["Druid Metrik einfügen"],"Edit Druid Metric":["Druid Metric bearbeiten"],"Whether the access to this metric is restricted to certain roles. Only roles with the permission 'metric access on XXX (the name of this metric)' are allowed to access this metric":[""],"Verbose Name":[""],"JSON":["JSON"],"Druid Datasource":["Druid Datenquelle"],"Warning Message":["Warnmeldung"],"List Druid Cluster":["Druid Cluster auflisten"],"Show Druid Cluster":["Druid Cluster anzeigen"],"Add Druid Cluster":["Druid Cluster einfügen"],"Edit Druid Cluster":["Druid Cluster bearbeiten"],"Cluster":[""],"Broker Host":[""],"Broker Port":[""],"Broker Endpoint":[""],"Druid Clusters":[""],"Sources":["Quellen"],"List Druid Datasource":["Druid Datenquellen auflisten"],"Show Druid Datasource":["Druid Datenquelle anzeigen"],"Add Druid Datasource":["Druid Datenquelle einfügen"],"Edit Druid Datasource":["Druid Datenquelle bearbeiten"],"The list of slices associated with this table. By altering this datasource, you may change how these associated slices behave. Also note that slices need to point to a datasource, so this form will fail at saving if removing slices from a datasource. If you want to change the datasource for a slice, overwrite the slice from the 'explore view'":[""],"Timezone offset (in hours) for this datasource":[""],"Time expression to use as a predicate when retrieving distinct values to populate the filter component. Only applies when `Enable Filter Select` is on. If you enter `7 days ago`, the distinct list of values in the filter will be populated based on the distinct value over the past week":[""],"Whether to populate the filter's dropdown in the + explore view's filter section with a list of distinct values fetched from the backend on the fly":[""],"Redirects to this endpoint when clicking on the datasource from the datasource list":[""],"Associated Slices":[""],"Data Source":["Datenquelle"],"Owner":[""],"Is Hidden":[""],"Enable Filter Select":[""],"Default Endpoint":[""],"Time Offset":[""],"Cache Timeout":[""],"Druid Datasources":["Druid Datenquellen"],"Scan New Datasources":["Neue Datenquellen suchen"],"Refresh Druid Metadata":["Druid Metadata aktualisieren"],"Datetime column not provided as part table configuration and is required by this type of chart":[""],"Empty query?":["Leere Abfrage?"],"Metric '{}' is not valid":["Metrik '{}' ist nicht valide"],"Table [{}] doesn't seem to exist in the specified database, couldn't fetch column information":[""],"List Columns":["Spalten auflisten"],"Show Column":["Spalte anzeigen"],"Add Column":["Spalte einfügen"],"Edit Column":["Spalte bearbeiten"],"Whether to make this column available as a [Time Granularity] option, column has to be DATETIME or DATETIME-like":[""],"The data type that was inferred by the database. It may be necessary to input a type manually for expression-defined columns in some cases. In most case users should not need to alter this.":[""],"Expression":[""],"Is temporal":[""],"Datetime Format":[""],"Database Expression":[""],"List Metrics":["Metriken auflisten"],"Show Metric":["Metrik anzeigen"],"Add Metric":["Metrik einfügen"],"Edit Metric":["Metrik bearbeiten"],"SQL Expression":[""],"D3 Format":[""],"Is Restricted":["Ist begrenzt"],"List Tables":["Tabellen auflisten"],"Show Table":["Tabelle anzeigen"],"Add Table":["Tabelle einfügen"],"Edit Table":["Tabelle bearbeiten"],"Name of the table that exists in the source database":[""],"Schema, as used only in some databases like Postgres, Redshift and DB2":[""],"This fields acts a Superset view, meaning that Superset will run a query against this string as a subquery.":[""],"Predicate applied when fetching distinct value to populate the filter control component. Supports jinja template syntax. Applies only when `Enable Filter Select` is on.":[""],"Redirects to this endpoint when clicking on the table from the table list":[""],"Changed By":["Bearbeitet von"],"Database":["Datenbank"],"Last Changed":["Bearbeitet am"],"Schema":[""],"Offset":[""],"Table Name":["Tabellenname"],"Fetch Values Predicate":[""],"Main Datetime Column":[""],"Table [{}] could not be found, please double check your database connection, schema, and table name":[""],"The table was created. As part of this two phase configuration process, you should now click the edit button by the new table to configure it.":[""],"Tables":["Tabellen"],"Profile":["Profil"],"Logout":["Abmelden"],"Login":["Anmelden"],"Record Count":["Datensätzeanzahl"],"No records found":["Keine Datensätze gefunden"],"Import":["Importieren"],"No Access!":["Keine Zugriff!"],"You do not have permissions to access the datasource(s): %(name)s.":[""],"Request Permissions":["Berechtigung anfordern"],"Welcome!":["Willkommen!"],"Test Connection":["Verbindungstest"],"Manage":["Einstellungen"],"Datasource %(name)s already exists":[""],"json isn't valid":[""],"Delete":["Löschen"],"Delete all Really?":["Wirklich alle löschen?"],"This endpoint requires the `all_datasource_access` permission":[""],"The datasource seems to have been deleted":[""],"The access requests seem to have been deleted":[""],"The user seems to have been deleted":[""],"You don't have access to this datasource":["Sie haben keine Zugriff auf diese Datenquelle"],"This view requires the database %(name)s or `all_datasource_access` permission":[""],"This endpoint requires the datasource %(name)s, database or `all_datasource_access` permission":[""],"List Databases":["Dakenbanken auflisten"],"Show Database":["Datenbank anzeigen"],"Add Database":["Datenbank einfügen"],"Edit Database":["Datenbank bearbeiten"],"Expose this DB in SQL Lab":[""],"Allow users to run synchronous queries, this is the default and should work well for queries that can be executed within a web request scope (<~1 minute)":[""],"Allow users to run queries, against an async backend. This assumes that you have a Celery worker setup as well as a results backend.":[""],"Allow CREATE TABLE AS option in SQL Lab":[""],"Allow users to run non-SELECT statements (UPDATE, DELETE, CREATE, ...) in SQL Lab":[""],"When allowing CREATE TABLE AS option in SQL Lab, this option forces the table to be created in this schema":[""],"All the queries in Sql Lab are going to be executed on behalf of currently authorized user.":[""],"Expose in SQL Lab":[""],"Allow CREATE TABLE AS":[""],"Allow DML":[""],"CTAS Schema":[""],"Creator":["Schöpfer"],"SQLAlchemy URI":[""],"Extra":[""],"Allow Run Sync":[""],"Allow Run Async":[""],"Impersonate queries to the database":[""],"Import Dashboards":["Dashboards importieren"],"User":["Benutzer"],"User Roles":["Benutzer Rollen"],"Database URL":["Datenbank URL"],"Roles to grant":[""],"Created On":[""],"Access requests":["Zugriffsanforderungen"],"Security":["Sicherheit"],"List Slices":["Schieben auflisten"],"Show Slice":["Schiebe anzeigen"],"Add Slice":["Schiebe einfügen"],"Edit Slice":["Schiebe bearbeiten"],"These parameters are generated dynamically when clicking the save or overwrite button in the explore view. This JSON object is exposed here for reference and for power users who may want to alter specific parameters.":[""],"Duration (in seconds) of the caching timeout for this slice.":[""],"Last Modified":["Geändert"],"Owners":[""],"Parameters":["Parameter"],"Slice":["Schiebe"],"List Dashboards":["Dashboards auflisten"],"Show Dashboard":["Dashboard anzeigen"],"Add Dashboard":["Dashboard einfügen"],"Edit Dashboard":["Dashboard bearbeiten"],"This json object describes the positioning of the widgets in the dashboard. It is dynamically generated when adjusting the widgets size and positions by using drag & drop in the dashboard view":[""],"The css for individual dashboards can be altered here, or in the dashboard view where changes are immediately visible":[""],"To get a readable URL for your dashboard":[""],"This JSON object is generated dynamically when clicking the save or overwrite button in the dashboard view. It is exposed here for reference and for power users who may want to alter specific parameters.":[""],"Owners is a list of users who can alter the dashboard.":[""],"Dashboard":[""],"Slug":[""],"Position JSON":[""],"JSON Metadata":[""],"Underlying Tables":[""],"Export":[""],"Export dashboards?":[""],"Action":["Aktion"],"dttm":[""],"Action Log":["Aktionsprotokoll"],"Access was requested":["Zugang wurde beantragt"],"%(user)s was granted the role %(role)s that gives access to the %(datasource)s":[""],"Role %(r)s was extended to provide the access to the datasource %(ds)s":[""],"You have no permission to approve this request":[""],"Malformed request. slice_id or table_name and db_name arguments are expected":[""],"Slice %(id)s not found":[""],"Table %(t)s wasn't found in the database %(d)s":[""],"Can't find User '%(name)s', please ask your admin to create one.":[""],"Can't find DruidCluster with cluster_name = '%(name)s'":[""],"Query record was not created as expected.":[""],"Template Name":["Vorlagename"],"CSS Templates":["CSS Vorlagen"],"SQL Editor":[""],"SQL Lab":[""],"Query Search":["Abfragen suchen"],"Status":[""],"Start Time":["Von Zeit"],"End Time":["Bis Zeit"],"Queries":["Abfragen"],"List Saved Query":["Gespeicherte Abfragen auflisten"],"Show Saved Query":["Gespeicherte Abfrage anzeigen"],"Add Saved Query":["Gespeicherte Abfrage einfügen"],"Edit Saved Query":["Gespeicherte Abfrage bearbeiten"],"Pop Tab Link":[""],"Changed on":["Bearbeitet am"],"Saved Queries":["Gespeicherte Abfragen"]}}} diff --git a/superset/translations/de/LC_MESSAGES/messages.mo b/superset/translations/de/LC_MESSAGES/messages.mo index 7f6c922d6c2bd7ae7e005025aacd424bfb52fd38..0d51828d0304bdd6c27ebc7312b66fb153cef9aa 100644 GIT binary patch delta 13097 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 delta 24211 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 ZKRY*S-b`O&hM;q!zD%rB-q$7b{{vt (获取权限) " msgid "You don't have access to this datasource" msgstr "你不能访问这个数据源" -#: superset/views/core.py:90 +#: superset/views/core.py:98 #, python-format msgid "" -"This view requires the database %(name)s or `all_datasource_access` " -"permission" +"This view requires the database %(name)s or " +"`all_datasource_access` permission" msgstr "此视图需要数据库 %(name)s或“all_datasource_access”权限" -#: superset/views/core.py:95 +#: superset/views/core.py:82 #, python-format -msgid "" -"This endpoint requires the datasource %(name)s, database or " -"`all_datasource_access` permission" -msgstr "此端点需要数据源 %(name)s、数据库或“all_datasource_access”权限" +msgid "This endpoint requires the `all_datasource_access` permission" +msgstr "此端点需要“all_datasource_access”权限" #: superset/views/core.py:189 msgid "List Databases" @@ -4853,10 +4851,11 @@ msgstr "" "这个JSON对象描述了部件在看板中的位置。它是动态生成的,可以通过拖放,在看板中" "调整整部件的大小和位置" -#: superset/views/core.py:552 +#: superset/views/core.py:627 msgid "" -"The css for individual dashboards can be altered here, or in the dashboard " -"view where changes are immediately visible" +"The CSS for individual dashboards can be altered here, or " +"in the dashboard view where changes are immediately " +"visible" msgstr "可以在这里或者在看板视图修改单个看板的CSS样式" #: superset/views/core.py:556 @@ -5054,6 +5053,83 @@ msgstr "改变为" msgid "Saved Queries" msgstr "已保存查询" +#: superset/views/core.py:299 +msgid "Chart Cache Timeout" +msgstr "表缓存超时" + +#: superset/views/core.py:284 +msgid "" +"Duration (in seconds) of the caching timeout for charts of this database. " +"A timeout of 0 indicates that the cache never expires. " +"Note this defaults to the global timeout if undefined." +msgstr "" +"此数据库图表的缓存超时持续时间(以秒为单位)。超时为0表示缓存永远不会过期。" +"注意,如果未定义,这默认为全局超时。" + +#: superset/views/core.py:303 +msgid "Allow Csv Upload" +msgstr "允许Csv上传" + +#: superset/views/core.py:288 +msgid "If selected, please set the schemas allowed for csv upload in Extra." +msgstr "如果选择,请额外设置csv上传允许的模式。" + +#: superset/views/core.py:301 +msgid "Asynchronous Query Execution" +msgstr "异步执行查询" + +#: superset/views/core.py:237 +msgid "" +"Operate the database in asynchronous mode, meaning " +"that the queries are executed on remote workers as opposed " +"to on the web server itself. " +"This assumes that you have a Celery worker setup as well " +"as a results backend. Refer to the installation docs " +"for more information." +msgstr "" +"以异步模式操作数据库,这意味着查询是在远程工作人员上执行的,而不是在web服务器本身上执行的, " +"这假设您有一个Celery worker setup以及一个结果后端。有关更多信息,请参考安装文档。" + +#: superset/connectors/sqla/views.py:173 +msgid "Import a table definition" +msgstr "导入一个已定义的表" + +#: superset/connectors/sqla/views.py:237 +msgid "" +"Duration (in seconds) of the caching timeout for this table. " +"A timeout of 0 indicates that the cache never expires. " +"Note this defaults to the database timeout if undefined." +msgstr "" +"此表的缓存超时持续时间(以秒为单位)。超时为0表示缓存永远不会过期。" +"注意,如果未定义,这默认为数据库的超时。" + +#: superset/views/core.py:520 +msgid "" +"Duration (in seconds) of the caching timeout for this chart. " +"Note this defaults to the datasource/table timeout if undefined." +msgstr "" +"此图表的缓存超时持续时间(以秒为单位)。" +"注意,如果未定义,这默认为数据源/表超时。" + +#: superset/connectors/druid/views.py:189 +msgid "" +"Duration (in seconds) of the caching timeout for this cluster. " +"A timeout of 0 indicates that the cache never expires. " +"Note this defaults to the global timeout if undefined." +msgstr "" +"此集群的缓存超时持续时间(以秒为单位)。超时为0表示缓存永远不会过期。" +"注意,如果未定义,这默认为全局超时。" + +#: superset/connectors/druid/views.py:276 +msgid "" +"Duration (in seconds) of the caching timeout for this datasource. " +"A timeout of 0 indicates that the cache never expires. " +"Note this defaults to the cluster timeout if undefined." +msgstr "" +"此数据源的缓存超时持续时间(以秒为单位)。超时为0表示缓存永远不会过期。" +"注意,如果未定义,这默认为集群超时。" + + #~ msgid "Please choose at least one \"Group by\" field" #~ msgstr "请至少选择一个“分组”字段" diff --git a/superset/utils/core.py b/superset/utils/core.py index 122998e2fea0e..3b4145793939a 100644 --- a/superset/utils/core.py +++ b/superset/utils/core.py @@ -33,6 +33,7 @@ import sys from time import struct_time from typing import List, Optional, Tuple +from urllib.parse import unquote_plus import uuid import zlib @@ -141,8 +142,18 @@ def wrapper(f): return wrapper -def js_string_to_python(item: str) -> Optional[str]: - return None if item in ('null', 'undefined') else item +def parse_js_uri_path_item(item: Optional[str], unquote: bool = True, + eval_undefined: bool = False) -> Optional[str]: + """Parse a uri path item made with js. + + :param item: a uri path component + :param unquote: Perform unquoting of string using urllib.parse.unquote_plus() + :param eval_undefined: When set to True and item is either 'null' or 'undefined', + assume item is undefined and return None. + :return: Either None, the original item or unquoted item + """ + item = None if eval_undefined and item in ('null', 'undefined') else item + return unquote_plus(item) if unquote and item else item def string_to_num(s: str): @@ -852,11 +863,6 @@ def merge_request_params(form_data: dict, params: dict): form_data['url_params'] = url_params -def get_update_perms_flag() -> bool: - val = os.environ.get('SUPERSET_UPDATE_PERMS') - return val.lower() not in ('0', 'false', 'no') if val else True - - def user_label(user: User) -> Optional[str]: """Given a user ORM FAB object, returns a label""" if user: diff --git a/superset/views/annotations.py b/superset/views/annotations.py index 8177efc883543..9ef2d65cadbba 100644 --- a/superset/views/annotations.py +++ b/superset/views/annotations.py @@ -18,12 +18,30 @@ from flask_appbuilder.models.sqla.interface import SQLAInterface from flask_babel import gettext as __ from flask_babel import lazy_gettext as _ +from wtforms.validators import StopValidation from superset import appbuilder from superset.models.annotations import Annotation, AnnotationLayer from .base import DeleteMixin, SupersetModelView +class StartEndDttmValidator(object): + """ + Validates dttm fields. + """ + def __call__(self, form, field): + if not form['start_dttm'].data and not form['end_dttm'].data: + raise StopValidation( + _('annotation start time or end time is required.'), + ) + elif (form['end_dttm'].data and + form['start_dttm'].data and + form['end_dttm'].data < form['start_dttm'].data): + raise StopValidation( + _('Annotation end time must be no earlier than start time.'), + ) + + class AnnotationModelView(SupersetModelView, DeleteMixin): # noqa datamodel = SQLAInterface(Annotation) @@ -53,17 +71,17 @@ class AnnotationModelView(SupersetModelView, DeleteMixin): # noqa annotation needs to add more context.', } + validators_columns = { + 'start_dttm': [ + StartEndDttmValidator(), + ], + } + def pre_add(self, obj): - if not obj.layer: - raise Exception('Annotation layer is required.') - if not obj.start_dttm and not obj.end_dttm: - raise Exception('Annotation start time or end time is required.') - elif not obj.start_dttm: + if not obj.start_dttm: obj.start_dttm = obj.end_dttm elif not obj.end_dttm: obj.end_dttm = obj.start_dttm - elif obj.end_dttm < obj.start_dttm: - raise Exception('Annotation end time must be no earlier than start time.') def pre_update(self, obj): self.pre_add(obj) diff --git a/superset/views/core.py b/superset/views/core.py index a7188d2b4c8dc..1ccfa1037ab83 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -1566,8 +1566,8 @@ def tables(self, db_id, schema, substr, force_refresh='false'): """Endpoint to fetch the list of tables for given database""" db_id = int(db_id) force_refresh = force_refresh.lower() == 'true' - schema = utils.js_string_to_python(schema) - substr = utils.js_string_to_python(substr) + schema = utils.parse_js_uri_path_item(schema, eval_undefined=True) + substr = utils.parse_js_uri_path_item(substr, eval_undefined=True) database = db.session.query(models.Database).filter_by(id=db_id).one() if schema: @@ -2350,11 +2350,11 @@ def sqllab_viz(self): })) @has_access - @expose('/table////') + @expose('/table////') @log_this def table(self, database_id, table_name, schema): - schema = utils.js_string_to_python(schema) - table_name = parse.unquote_plus(table_name) + schema = utils.parse_js_uri_path_item(schema, eval_undefined=True) + table_name = utils.parse_js_uri_path_item(table_name) mydb = db.session.query(models.Database).filter_by(id=database_id).one() payload_columns = [] indexes = [] @@ -2410,11 +2410,11 @@ def table(self, database_id, table_name, schema): return json_success(json.dumps(tbl)) @has_access - @expose('/extra_table_metadata////') + @expose('/extra_table_metadata////') @log_this def extra_table_metadata(self, database_id, table_name, schema): - schema = utils.js_string_to_python(schema) - table_name = parse.unquote_plus(table_name) + schema = utils.parse_js_uri_path_item(schema, eval_undefined=True) + table_name = utils.parse_js_uri_path_item(table_name) mydb = db.session.query(models.Database).filter_by(id=database_id).one() payload = mydb.db_engine_spec.extra_table_metadata( mydb, table_name, schema) @@ -2427,6 +2427,8 @@ def extra_table_metadata(self, database_id, table_name, schema): def select_star(self, database_id, table_name, schema=None): mydb = db.session.query( models.Database).filter_by(id=database_id).first() + schema = utils.parse_js_uri_path_item(schema, eval_undefined=True) + table_name = utils.parse_js_uri_path_item(table_name) return json_success( mydb.select_star( table_name, @@ -2577,8 +2579,9 @@ def validate_sql_json(self): except Exception as e: logging.exception(e) msg = _( - f'{validator.name} was unable to check your query.\nPlease ' - 'make sure that any services it depends on are available\n' + 'Failed to validate your SQL query text. Please check that ' + f'you have configured the {validator.name} validator ' + 'correctly and that any services it depends on are up. ' f'Exception: {e}') return json_error_response(f'{msg}') diff --git a/superset/viz.py b/superset/viz.py index 123c361d1bdcb..9661e238f84c7 100644 --- a/superset/viz.py +++ b/superset/viz.py @@ -213,7 +213,7 @@ def get_df(self, query_obj=None): try: int(one_ts_val) is_integral = True - except ValueError: + except (ValueError, TypeError): is_integral = False if is_integral: unit = 's' if timestamp_format == 'epoch_s' else 'ms' diff --git a/tests/security_tests.py b/tests/security_tests.py index 57b790cf7b130..6912deefc1def 100644 --- a/tests/security_tests.py +++ b/tests/security_tests.py @@ -249,6 +249,8 @@ def test_views_are_secured(self): ['Superset', 'log'], ['Superset', 'theme'], ['Superset', 'welcome'], + ['SecurityApi', 'login'], + ['SecurityApi', 'refresh'], ] unsecured_views = [] for view_class in appbuilder.baseviews: diff --git a/tests/sql_parse_tests.py b/tests/sql_parse_tests.py index 56959397fa9c5..7096147b5615f 100644 --- a/tests/sql_parse_tests.py +++ b/tests/sql_parse_tests.py @@ -462,3 +462,12 @@ def test_messy_breakdown_statements(self): 'SELECT * FROM ab_user LIMIT 1', ] self.assertEquals(statements, expected) + + def test_identifier_list_with_keyword_as_alias(self): + query = """ + WITH + f AS (SELECT * FROM foo), + match AS (SELECT * FROM f) + SELECT * FROM match + """ + self.assertEquals({'foo'}, self.extract_tables(query)) diff --git a/tests/utils_tests.py b/tests/utils_tests.py index 7e2908984f424..40dedafaef597 100644 --- a/tests/utils_tests.py +++ b/tests/utils_tests.py @@ -35,6 +35,7 @@ merge_extra_filters, merge_request_params, parse_human_timedelta, + parse_js_uri_path_item, validate_json, zlib_compress, zlib_decompress_to_string, @@ -756,3 +757,18 @@ def test_convert_legacy_filters_into_adhoc_present_and_nonempty(self): } convert_legacy_filters_into_adhoc(form_data) self.assertEquals(form_data, expected) + + def test_parse_js_uri_path_items_eval_undefined(self): + self.assertIsNone(parse_js_uri_path_item('undefined', eval_undefined=True)) + self.assertIsNone(parse_js_uri_path_item('null', eval_undefined=True)) + self.assertEqual('undefined', parse_js_uri_path_item('undefined')) + self.assertEqual('null', parse_js_uri_path_item('null')) + + def test_parse_js_uri_path_items_unquote(self): + self.assertEqual('slashed/name', parse_js_uri_path_item('slashed%2fname')) + self.assertEqual('slashed%2fname', parse_js_uri_path_item('slashed%2fname', + unquote=False)) + + def test_parse_js_uri_path_items_item_optional(self): + self.assertIsNone(parse_js_uri_path_item(None)) + self.assertIsNotNone(parse_js_uri_path_item('item'))