diff --git a/.github/prlint.json b/.github/prlint.json deleted file mode 100644 index c1f6927624c66..0000000000000 --- a/.github/prlint.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "title": [ - { - "pattern": "^(build|chore|ci|docs|feat|fix|perf|refactor|style|test|other)((.+))?:\\s.+", - "message": "Your title needs to be prefixed with a topic." - } - ] -} diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml new file mode 100644 index 0000000000000..ec4926d075dc0 --- /dev/null +++ b/.github/workflows/pr-lint.yml @@ -0,0 +1,22 @@ +name: PR Lint + +on: + pull_request: + # By default, a workflow only runs when a pull_request's activity type is opened, synchronize, or reopened. We + # explicity override here so that PR titles are re-linted when the PR text content is edited. + # + # Possible values: https://help.github.com/en/actions/reference/events-that-trigger-workflows#pull-request-event-pull_request + types: [opened, edited, reopened, synchronize] + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: morrisoncole/pr-lint-action@v1.3.0 + with: + title-regex: "^(build|chore|ci|docs|feat|fix|perf|refactor|style|test|other)((.+))?:\\s.+" + on-failed-regex-fail-action: true + on-failed-regex-create-review: true + on-failed-regex-comment: + "Please format your PR title to match: `%regex%`!" + repo-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/prefer-typescript.yml b/.github/workflows/prefer-typescript.yml index d2fff6e4565b0..c6bab635590e3 100644 --- a/.github/workflows/prefer-typescript.yml +++ b/.github/workflows/prefer-typescript.yml @@ -11,7 +11,7 @@ jobs: steps: - name: Get changed files id: changed - uses: trilom/file-changes-action@master + uses: trilom/file-changes-action@v1.2.4 with: githubToken: ${{ secrets.GITHUB_TOKEN }} @@ -21,7 +21,9 @@ jobs: js_files_added() { jq -r ' map( - select((endswith(".js") or endswith(".jsx")) + select( + endswith(".js") or endswith(".jsx") + ) ) | join("\n") ' ${HOME}/files_added.json } diff --git a/.github/workflows/superset-docs.yml b/.github/workflows/superset-docs.yml new file mode 100644 index 0000000000000..95039732a1c49 --- /dev/null +++ b/.github/workflows/superset-docs.yml @@ -0,0 +1,30 @@ +name: Docs + +on: + push: + paths: + - 'docs/**' + pull_request: + paths: + - 'docs/**' + +jobs: + docs: + name: build + runs-on: ubuntu-18.04 + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Install dependencies + - name: npm install + working-directory: ./docs + run: | + npm install + - name: lint + working-directory: ./docs + run: | + npm run lint + - name: gatsby build + working-directory: ./docs + run: | + npm run build diff --git a/.github/workflows/superset-e2e.yml b/.github/workflows/superset-e2e.yml index dfa691058ac65..f634e1544cc25 100644 --- a/.github/workflows/superset-e2e.yml +++ b/.github/workflows/superset-e2e.yml @@ -1,6 +1,12 @@ name: E2E -on: [push, pull_request] +on: + push: + paths-ignore: + - 'docs/**' + pull_request: + paths-ignore: + - 'docs/**' jobs: Cypress: diff --git a/.github/workflows/superset-frontend.yml b/.github/workflows/superset-frontend.yml index 86d0e533cd84a..0f0b5db48548b 100644 --- a/.github/workflows/superset-frontend.yml +++ b/.github/workflows/superset-frontend.yml @@ -1,6 +1,12 @@ name: Frontend -on: [push, pull_request] +on: + push: + paths-ignore: + - 'docs/**' + pull_request: + paths-ignore: + - 'docs/**' jobs: build: @@ -25,18 +31,3 @@ jobs: working-directory: ./superset-frontend run: | bash <(curl -s https://codecov.io/bash) -cF javascript - - docs: - name: build - runs-on: ubuntu-18.04 - steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: Install dependencies - working-directory: ./superset-frontend - run: | - npm install - - name: gatsby build - working-directory: ./superset-frontend - run: | - npm run build diff --git a/.github/workflows/superset-python.yml b/.github/workflows/superset-python.yml index 6bfe3772185a7..5da41d9f5c93b 100644 --- a/.github/workflows/superset-python.yml +++ b/.github/workflows/superset-python.yml @@ -1,7 +1,13 @@ # Python unit tests name: Python -on: [push, pull_request] +on: + push: + paths-ignore: + - 'docs/**' + pull_request: + paths-ignore: + - 'docs/**' jobs: lint: diff --git a/.github/workflows/test-hive.yml b/.github/workflows/test-hive.yml index 2571f4a2f85ee..b372be4519f56 100644 --- a/.github/workflows/test-hive.yml +++ b/.github/workflows/test-hive.yml @@ -1,6 +1,12 @@ name: Hive -on: [push, pull_request] +on: + push: + paths-ignore: + - 'docs/**' + pull_request: + paths-ignore: + - 'docs/**' jobs: test-postgres-hive: diff --git a/.github/workflows/test-presto.yml b/.github/workflows/test-presto.yml index 14ac0a8f5eafa..360026b056cfc 100644 --- a/.github/workflows/test-presto.yml +++ b/.github/workflows/test-presto.yml @@ -1,6 +1,12 @@ name: Presto -on: [push, pull_request] +on: + push: + paths-ignore: + - 'docs/**' + pull_request: + paths-ignore: + - 'docs/**' jobs: test-postgres-presto: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5edd6d47b5a05..c9de04e4de247 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,8 +34,8 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v3.2.0 hooks: - - id: check-added-large-files - id: check-docstring-first + - id: check-added-large-files - id: check-yaml exclude: ^helm/superset/templates/ - id: debug-statements diff --git a/.pylintrc b/.pylintrc index 07658117c9eb3..e69d5c484411b 100644 --- a/.pylintrc +++ b/.pylintrc @@ -81,7 +81,7 @@ confidence= # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" -disable=standarderror-builtin,long-builtin,dict-view-method,intern-builtin,suppressed-message,no-absolute-import,unpacking-in-except,apply-builtin,delslice-method,indexing-exception,old-raise-syntax,print-statement,cmp-builtin,reduce-builtin,useless-suppression,coerce-method,input-builtin,cmp-method,raw_input-builtin,nonzero-method,backtick,basestring-builtin,setslice-method,reload-builtin,oct-method,map-builtin-not-iterating,execfile-builtin,old-octal-literal,zip-builtin-not-iterating,buffer-builtin,getslice-method,metaclass-assignment,xrange-builtin,long-suffix,round-builtin,range-builtin-not-iterating,next-method-called,dict-iter-method,parameter-unpacking,unicode-builtin,unichr-builtin,import-star-module-level,raising-string,filter-builtin-not-iterating,using-cmp-argument,coerce-builtin,file-builtin,old-division,hex-method,invalid-unary-operand-type,missing-docstring,too-many-lines,duplicate-code,bad-continuation,ungrouped-imports,import-outside-toplevel,raise-missing-from,super-with-arguments,bad-option-value +disable=standarderror-builtin,long-builtin,dict-view-method,intern-builtin,suppressed-message,no-absolute-import,unpacking-in-except,apply-builtin,delslice-method,indexing-exception,old-raise-syntax,print-statement,cmp-builtin,reduce-builtin,useless-suppression,coerce-method,input-builtin,cmp-method,raw_input-builtin,nonzero-method,backtick,basestring-builtin,setslice-method,reload-builtin,oct-method,map-builtin-not-iterating,execfile-builtin,old-octal-literal,zip-builtin-not-iterating,buffer-builtin,getslice-method,metaclass-assignment,xrange-builtin,long-suffix,round-builtin,range-builtin-not-iterating,next-method-called,parameter-unpacking,unicode-builtin,unichr-builtin,import-star-module-level,raising-string,filter-builtin-not-iterating,using-cmp-argument,coerce-builtin,file-builtin,old-division,hex-method,missing-docstring,too-many-lines,ungrouped-imports,import-outside-toplevel,raise-missing-from,super-with-arguments,bad-option-value [REPORTS] diff --git a/README.md b/README.md index b0ebf49cf9483..990d151184443 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,10 @@ A modern, enterprise-ready business intelligence web application. ## Screenshots & Gifs +**Gallery** + +
+ **View Dashboards**
diff --git a/docs/README.md b/docs/README.md index 738d41d743f20..6e754f7f40860 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,19 +1,21 @@ -[//]: # 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. + Here's the source to the documentation hosted at superset.apache.org diff --git a/docs/gatsby-node.js b/docs/gatsby-node.js index 4c1e45c4db1fd..67aa8f9259499 100644 --- a/docs/gatsby-node.js +++ b/docs/gatsby-node.js @@ -24,4 +24,589 @@ exports.createPages = ({ actions }) => { toPath: '/docs/installation/installing-superset-using-docker-compose', isPermanent: true, }); + createRedirect({ + fromPath: '/installation.html#getting-started', + toPath: '/docs/installation/installing-superset-using-docker-compose', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#cloud-native', + toPath: '/docs/installation/installing-superset-using-docker-compose', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#start-with-docker', + toPath: '/docs/installation/installing-superset-using-docker-compose', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#os-dependencies', + toPath: '/docs/installation/installing-superset-from-scratch#installing-superset-from-scratch', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#python-virtualenv', + toPath: '/docs/installation/installing-superset-from-scratch#installing-superset-from-scratch', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#python-s-setup-tools-and-pip', + toPath: '/docs/installation/installing-superset-from-scratch#installing-superset-from-scratch', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#superset-installation-and-initialization', + toPath: '/docs/installation/installing-superset-from-scratch#installing-superset-from-scratch', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#a-proper-wsgi-http-server', + toPath: '/docs/installation/configuring-superset', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#flask-appbuilder-permissions', + toPath: '/docs/installation/configuring-superset', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#configuration-behind-a-load-balancer', + toPath: '/docs/installation/configuring-superset', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#configuration', + toPath: '/docs/installation/configuring-superset', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#database-dependencies', + toPath: '/docs/databases/installing-database-drivers', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#hana', + toPath: '/docs/databases/hana', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#aws-athena', + toPath: '/docs/databases/athena', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#google-bigquery', + toPath: '/docs/databases/bigquery', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#elasticsearch', + toPath: '/docs/databases/elasticsearch', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#snowflake', + toPath: '/docs/databases/snowflake', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#teradata', + toPath: '/docs/databases/teradata', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#apache-drill', + toPath: '/docs/databases/drill', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#caching', + toPath: '/docs/installation/cache', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#deeper-sqlalchemy-integration', + toPath: '/docs/databases/extra-settings', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#schemas-postgres-redshift', + toPath: '/docs/databases/extra-settings', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#external-password-store-for-sqlalchemy-connections', + toPath: '/docs/databases/extra-settings', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#ssl-access-to-databases', + toPath: '/docs/databases/extra-settings', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#druid', + toPath: '/docs/databases/druid', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#presto', + toPath: '/docs/databases/presto', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#exasol', + toPath: '/docs/databases/exasol', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#cors', + toPath: '/docs/installation/networking-settings', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#domain-sharding', + toPath: '/docs/installation/networking-settings', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#middleware', + toPath: '/docs/installation/networking-settings', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#event-logging', + toPath: '/docs/installation/event-logging', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#upgrading', + toPath: '/docs/installation/upgrading-superset', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#celery-tasks', + toPath: '/docs/installation/async-queries-celery', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#email-reports', + toPath: '/docs/installation/email-reports', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#sql-lab', + toPath: '/docs/installation/sql-templating', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#celery-flower', + toPath: '/docs/installation/async-queries-celery', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#building-from-source', + toPath: '/docs/contribution', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#blueprints', + toPath: '/docs/installation/configuring-superset', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#statsd-logging', + toPath: '/docs/installation/event-logging', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#install-superset-with-helm-in-kubernetes', + toPath: '/docs/installation/installing-superset-from-scratch', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#custom-oauth2-configuration', + toPath: '/docs/installation/configuring-superset', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#feature-flags', + toPath: '/docs/installation/configuring-superset', + isPermanent: true, + }); + createRedirect({ + fromPath: '/installation.html#sip-15', + toPath: '/docs/installation/configuring-superset', + isPermanent: true, + }); + createRedirect({ + fromPath: '/tutorials.html', + toPath: '/docs/intro', + isPermanent: true, + }); + createRedirect({ + fromPath: '/admintutorial.html', + toPath: '/docs/creating-charts-dashboards/first-dashboard', + isPermanent: true, + }); + createRedirect({ + fromPath: '/admintutorial.html#connecting-to-a-new-database', + toPath: '/docs/creating-charts-dashboards/first-dashboard', + isPermanent: true, + }); + createRedirect({ + fromPath: '/admintutorial.html#adding-a-new-table', + toPath: '/docs/creating-charts-dashboards/first-dashboard', + isPermanent: true, + }); + createRedirect({ + fromPath: '/admintutorial.html#exploring-your-data', + toPath: '/docs/creating-charts-dashboards/first-dashboard', + isPermanent: true, + }); + createRedirect({ + fromPath: '/admintutorial.html#creating-a-slice-and-dashboard', + toPath: '/docs/creating-charts-dashboards/first-dashboard', + isPermanent: true, + }); + createRedirect({ + fromPath: '/usertutorial.html', + toPath: '/docs/creating-charts-dashboards/first-dashboard', + isPermanent: true, + }); + createRedirect({ + fromPath: '/usertutorial.html#enabling-upload-a-csv-functionality', + toPath: '/docs/creating-charts-dashboards/exploring-data', + isPermanent: true, + }); + createRedirect({ + fromPath: '/usertutorial.html#obtaining-and-loading-the-data', + toPath: '/docs/creating-charts-dashboards/exploring-data', + isPermanent: true, + }); + createRedirect({ + fromPath: '/usertutorial.html#table-visualization', + toPath: '/docs/creating-charts-dashboards/exploring-data', + isPermanent: true, + }); + createRedirect({ + fromPath: '/usertutorial.html#dashboard-basics', + toPath: '/docs/creating-charts-dashboards/exploring-data', + isPermanent: true, + }); + createRedirect({ + fromPath: '/usertutorial.html#pivot-table', + toPath: '/docs/creating-charts-dashboards/exploring-data', + isPermanent: true, + }); + createRedirect({ + fromPath: '/usertutorial.html#line-chart', + toPath: '/docs/creating-charts-dashboards/exploring-data', + isPermanent: true, + }); + createRedirect({ + fromPath: '/usertutorial.html#markup', + toPath: '/docs/creating-charts-dashboards/exploring-data', + isPermanent: true, + }); + createRedirect({ + fromPath: '/usertutorial.html#filter-box', + toPath: '/docs/creating-charts-dashboards/exploring-data', + isPermanent: true, + }); + createRedirect({ + fromPath: '/usertutorial.html#publishing-your-dashboard', + toPath: '/docs/creating-charts-dashboards/exploring-data', + isPermanent: true, + }); + createRedirect({ + fromPath: '/usertutorial.html#taking-your-dashboard-further', + toPath: '/docs/creating-charts-dashboards/exploring-data', + isPermanent: true, + }); + createRedirect({ + fromPath: '/usertutorial.html#annotations', + toPath: '/docs/creating-charts-dashboards/exploring-data', + isPermanent: true, + }); + createRedirect({ + fromPath: '/usertutorial.html#advanced-analytics', + toPath: '/docs/creating-charts-dashboards/exploring-data', + isPermanent: true, + }); + createRedirect({ + fromPath: '/usertutorial.html#setting-up-the-base-chart', + toPath: '/docs/creating-charts-dashboards/exploring-data', + isPermanent: true, + }); + createRedirect({ + fromPath: '/usertutorial.html#rolling-mean', + toPath: '/docs/creating-charts-dashboards/exploring-data', + isPermanent: true, + }); + createRedirect({ + fromPath: '/usertutorial.html#time-comparison', + toPath: '/docs/creating-charts-dashboards/exploring-data', + isPermanent: true, + }); + createRedirect({ + fromPath: '/usertutorial.html#resampling-the-data', + toPath: '/docs/creating-charts-dashboards/exploring-data', + isPermanent: true, + }); + createRedirect({ + fromPath: '/security.html', + toPath: '/docs/security', + isPermanent: true, + }); + createRedirect({ + fromPath: '/security.html#provided-roles', + toPath: '/docs/security', + isPermanent: true, + }); + createRedirect({ + fromPath: '/security.html#admin', + toPath: '/docs/security', + isPermanent: true, + }); + createRedirect({ + fromPath: '/security.html#alpha', + toPath: '/docs/security', + isPermanent: true, + }); + createRedirect({ + fromPath: '/security.html#gamma', + toPath: '/docs/security', + isPermanent: true, + }); + createRedirect({ + fromPath: '/security.html#sql-lab', + toPath: '/docs/security', + isPermanent: true, + }); + createRedirect({ + fromPath: '/security.html#public', + toPath: '/docs/security', + isPermanent: true, + }); + createRedirect({ + fromPath: '/security.html#managing-gamma-per-data-source-access', + toPath: '/docs/security', + isPermanent: true, + }); + createRedirect({ + fromPath: '/security.html#customizing', + toPath: '/docs/security', + isPermanent: true, + }); + createRedirect({ + fromPath: '/security.html#permissions', + toPath: '/docs/security', + isPermanent: true, + }); + createRedirect({ + fromPath: '/security.html#restricting-access-to-a-subset-of-data-sources', + toPath: '/docs/security', + isPermanent: true, + }); + createRedirect({ + fromPath: '/sqllab.html', + toPath: '/docs/installation/sql-templating', + isPermanent: true, + }); + createRedirect({ + fromPath: '/sqllab.html#feature-overview', + toPath: '/docs/installation/sql-templating', + isPermanent: true, + }); + createRedirect({ + fromPath: '/sqllab.html#extra-features', + toPath: '/docs/installation/sql-templating', + isPermanent: true, + }); + createRedirect({ + fromPath: '/sqllab.html#templating-with-jinja', + toPath: '/docs/installation/sql-templating', + isPermanent: true, + }); + createRedirect({ + fromPath: '/sqllab.html#available-macros', + toPath: '/docs/installation/sql-templating', + isPermanent: true, + }); + createRedirect({ + fromPath: '/sqllab.html#extending-macros', + toPath: '/docs/installation/sql-templating', + isPermanent: true, + }); + createRedirect({ + fromPath: '/sqllab.html#query-cost-estimation', + toPath: '/docs/installation/sql-templating', + isPermanent: true, + }); + createRedirect({ + fromPath: '/gallery.html', + toPath: '/docs/intro', + isPermanent: true, + }); + createRedirect({ + fromPath: '/druid.html', + toPath: '/docs/databases/druid', + isPermanent: true, + }); + createRedirect({ + fromPath: '/druid.html#aggregations', + toPath: '/docs/databases/druid', + isPermanent: true, + }); + createRedirect({ + fromPath: '/druid.html#post-aggregations', + toPath: '/docs/databases/druid', + isPermanent: true, + }); + createRedirect({ + fromPath: '/druid.html#unsupported-features', + toPath: '/docs/databases/druid', + isPermanent: true, + }); + createRedirect({ + fromPath: '/misc.html', + toPath: '/docs/miscellaneous/country-map-tools', + isPermanent: true, + }); + createRedirect({ + fromPath: '/visualization.html', + toPath: '/docs/miscellaneous/country-map-tools', + isPermanent: true, + }); + createRedirect({ + fromPath: '/visualization.html#country-map-tools', + toPath: '/docs/miscellaneous/country-map-tools', + isPermanent: true, + }); + createRedirect({ + fromPath: '/visualization.html#list-of-countries', + toPath: '/docs/miscellaneous/country-map-tools', + isPermanent: true, + }); + createRedirect({ + fromPath: '/visualization.html#need-to-add-a-new-country', + toPath: '/docs/miscellaneous/country-map-tools', + isPermanent: true, + }); + createRedirect({ + fromPath: '/videos.html', + toPath: '/resources', + isPermanent: true, + }); + createRedirect({ + fromPath: '/import_export_datasources.html#exporting-datasources-to-yaml', + toPath: '/docs/miscellaneous/importing-exporting-datasources', + isPermanent: true, + }); + createRedirect({ + fromPath: '/import_export_datasources.html#exporting-the-complete-supported-yaml-schema', + toPath: '/docs/miscellaneous/importing-exporting-datasources', + isPermanent: true, + }); + createRedirect({ + fromPath: '/import_export_datasources.html#importing-datasources-from-yaml', + toPath: '/docs/miscellaneous/importing-exporting-datasources', + isPermanent: true, + }); + createRedirect({ + fromPath: '/faq.html', + toPath: '/docs/frequently-asked-questions', + isPermanent: true, + }); + createRedirect({ + fromPath: '/faq.html#can-i-query-join-multiple-tables-at-one-time', + toPath: '/docs/frequently-asked-questions', + isPermanent: true, + }); + createRedirect({ + fromPath: '/faq.html#how-big-can-my-data-source-be', + toPath: '/docs/frequently-asked-questions', + isPermanent: true, + }); + createRedirect({ + fromPath: '/faq.html#how-do-i-create-my-own-visualization', + toPath: '/docs/frequently-asked-questions', + isPermanent: true, + }); + createRedirect({ + fromPath: '/faq.html#can-i-upload-and-visualize-csv-data', + toPath: '/docs/frequently-asked-questions', + isPermanent: true, + }); + createRedirect({ + fromPath: '/faq.html#why-are-my-queries-timing-out', + toPath: '/docs/frequently-asked-questions', + isPermanent: true, + }); + createRedirect({ + fromPath: '/faq.html#why-is-the-map-not-visible-in-the-mapbox-visualization', + toPath: '/docs/frequently-asked-questions', + isPermanent: true, + }); + createRedirect({ + fromPath: '/faq.html#how-to-add-dynamic-filters-to-a-dashboard', + toPath: '/docs/frequently-asked-questions', + isPermanent: true, + }); + createRedirect({ + fromPath: '/faq.html#how-to-limit-the-timed-refresh-on-a-dashboard', + toPath: '/docs/frequently-asked-questions', + isPermanent: true, + }); + createRedirect({ + fromPath: '/faq.html#why-does-flask-fab-or-superset-freezed-hung-not-responding-when-started-my-home-directory-is-nfs-mounted', + toPath: '/docs/frequently-asked-questions', + isPermanent: true, + }); + createRedirect({ + fromPath: '/faq.html#what-if-the-table-schema-changed', + toPath: '/docs/frequently-asked-questions', + isPermanent: true, + }); + createRedirect({ + fromPath: '/faq.html#how-do-i-go-about-developing-a-new-visualization-type', + toPath: '/docs/frequently-asked-questions', + isPermanent: true, + }); + createRedirect({ + fromPath: '/faq.html#what-database-engine-can-i-use-as-a-backend-for-superset', + toPath: '/docs/frequently-asked-questions', + isPermanent: true, + }); + createRedirect({ + fromPath: '/faq.html#how-can-i-configure-oauth-authentication-and-authorization', + toPath: '/docs/frequently-asked-questions', + isPermanent: true, + }); + createRedirect({ + fromPath: '/faq.html#how-can-i-set-a-default-filter-on-my-dashboard', + toPath: '/docs/frequently-asked-questions', + isPermanent: true, + }); + createRedirect({ + fromPath: 'faq.html#how-do-i-get-superset-to-refresh-the-schema-of-my-table', + toPath: '/docs/frequently-asked-questions', + isPermanent: true, + }); + createRedirect({ + fromPath: '/faq.html#is-there-a-way-to-force-the-use-specific-colors', + toPath: '/docs/frequently-asked-questions', + isPermanent: true, + }); + createRedirect({ + fromPath: '/faq.html#does-superset-work-with-insert-database-engine-here', + toPath: '/docs/frequently-asked-questions', + isPermanent: true, + }); + createRedirect({ + fromPath: '/index.html', + toPath: '/docs/intro', + isPermanent: true, + }); }; diff --git a/docs/package-lock.json b/docs/package-lock.json index 9129e4c2d43b7..ef19633b21468 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -3157,6 +3157,15 @@ "normalize-path": "^2.1.1" } }, + "aphrodite": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/aphrodite/-/aphrodite-0.5.0.tgz", + "integrity": "sha1-pLmokCZiOV0nAucKx6K0ymbyVwM=", + "requires": { + "asap": "^2.0.3", + "inline-style-prefixer": "^2.0.0" + } + }, "application-config-path": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/application-config-path/-/application-config-path-0.1.0.tgz", @@ -4318,6 +4327,11 @@ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" }, + "bowser": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-1.9.4.tgz", + "integrity": "sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ==" + }, "boxen": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", @@ -7281,6 +7295,14 @@ "utila": "~0.4" } }, + "dom-helpers": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", + "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", + "requires": { + "@babel/runtime": "^7.1.2" + } + }, "dom-iterator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dom-iterator/-/dom-iterator-1.0.0.tgz", @@ -8405,6 +8427,11 @@ } } }, + "exenv": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha1-KueOhdmJQVhnCwPUe+wfA72Ru50=" + }, "exif-parser": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz", @@ -12267,6 +12294,11 @@ } } }, + "hyphenate-style-name": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", + "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -12880,6 +12912,15 @@ "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" }, + "inline-style-prefixer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-2.0.5.tgz", + "integrity": "sha1-wVPH6I/YT+9cYC6VqBaLJ3BnH+c=", + "requires": { + "bowser": "^1.0.0", + "hyphenate-style-name": "^1.0.1" + } + }, "inquirer": { "version": "7.3.3", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", @@ -18424,6 +18465,15 @@ "github-buttons": "^2.8.0" } }, + "react-grid-gallery": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/react-grid-gallery/-/react-grid-gallery-0.5.5.tgz", + "integrity": "sha512-DkKg2/Am+VZPDG39fazelTcsZSQrfM/YllnIcWToyUEfOZcrzHxUoqCziCkuTPmCuMbHnrjidBFuDbAFgvSnvQ==", + "requires": { + "prop-types": "^15.5.8", + "react-images": "^0.5.16" + } + }, "react-helmet": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz", @@ -18491,6 +18541,17 @@ "camelcase": "^5.0.0" } }, + "react-images": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/react-images/-/react-images-0.5.19.tgz", + "integrity": "sha512-B3d4W1uFJj+m17K8S65iAyEJShKGBjPk7n7N1YsPiAydEm8mIq9a6CoeQFMY1d7N2QMs6FBCjT9vELyc5jP5JA==", + "requires": { + "aphrodite": "^0.5.0", + "prop-types": "^15.6.0", + "react-scrolllock": "^2.0.1", + "react-transition-group": "2" + } + }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -18545,6 +18606,11 @@ } } }, + "react-prop-toggle": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/react-prop-toggle/-/react-prop-toggle-1.0.2.tgz", + "integrity": "sha512-JmerjAXs7qJ959+d0Ygt7Cb2+4fG+n3I2VXO6JO0AcAY1vkRN/JpZKAN67CMXY889xEJcfylmMPhzvf6nWO68Q==" + }, "react-reconciler": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.25.1.tgz", @@ -18605,6 +18671,15 @@ "resize-observer-polyfill": "^1.5.1" } }, + "react-scrolllock": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/react-scrolllock/-/react-scrolllock-2.0.7.tgz", + "integrity": "sha512-Gzpu8+ulxdYcybAgJOFTXc70xs7SBZDQbZNpKzchZUgLCJKjz6lrgESx6LHHZgfELx1xYL4yHu3kYQGQPFas/g==", + "requires": { + "exenv": "^1.2.2", + "react-prop-toggle": "^1.0.2" + } + }, "react-side-effect": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.0.tgz", @@ -18625,6 +18700,17 @@ "tslib": "^1.0.0" } }, + "react-transition-group": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz", + "integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==", + "requires": { + "dom-helpers": "^3.4.0", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2", + "react-lifecycles-compat": "^3.0.4" + } + }, "read": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", diff --git a/docs/package.json b/docs/package.json index b5dbeeb8af9bd..98de586575052 100644 --- a/docs/package.json +++ b/docs/package.json @@ -32,6 +32,7 @@ "react": "^16.12.0", "react-dom": "^16.12.0", "react-github-btn": "^1.2.0", + "react-grid-gallery": "^0.5.5", "react-helmet": "^6.1.0", "theme-ui": "^0.3.1", "three": "^0.68.0" diff --git a/docs/src/components/DbImage.tsx b/docs/src/components/DbImage.tsx new file mode 100644 index 0000000000000..d74a7173d5b11 --- /dev/null +++ b/docs/src/components/DbImage.tsx @@ -0,0 +1,49 @@ +/** + * 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 React from 'react'; +import { useStaticQuery, graphql } from 'gatsby'; +import Img from 'gatsby-image'; + +interface Props { + imageName?: string; +} + +const DbImage = ({ imageName }: Props) => { + const data = useStaticQuery(graphql` + query { + allImages: allFile(filter: {relativeDirectory: {eq: "src/images/databases"}}) { + edges { + node { + childImageSharp { + fixed(height: 50) { + ...GatsbyImageSharpFixed + originalName + } + } + } + } + } + } + `); + const images = data.allImages.edges.map((img) => img.node?.childImageSharp?.fixed); + const filter = images.filter((img) => img?.originalName === imageName); + return ; +}; + +export default DbImage; diff --git a/docs/src/components/MainMenu.tsx b/docs/src/components/MainMenu.tsx index 0de95c3510c99..46e7d5b7bba43 100644 --- a/docs/src/components/MainMenu.tsx +++ b/docs/src/components/MainMenu.tsx @@ -81,6 +81,9 @@ const MenuItems = ({ mode, toggleDrawer }: menuProps) => { Documentation + + Gallery + Community diff --git a/docs/src/components/footer.tsx b/docs/src/components/footer.tsx index e593f20405a50..a8778bdf623e6 100644 --- a/docs/src/components/footer.tsx +++ b/docs/src/components/footer.tsx @@ -28,6 +28,8 @@ const footerStyle = css` text-align: center; color: #ccc; padding: 10px; + height: 100%; + width: 100%; `; const copyrightStyle = css` diff --git a/docs/src/components/image.tsx b/docs/src/components/image.tsx index dc76bee666551..adc03a5932466 100644 --- a/docs/src/components/image.tsx +++ b/docs/src/components/image.tsx @@ -22,14 +22,13 @@ import Img from 'gatsby-image'; interface Props { imageName?: string; - type?: string; width?: string; height?: string; otherProps?: any; } const Image = ({ - imageName, type, width, height, ...otherProps + imageName, width, height, ...otherProps }: Props) => { const data = useStaticQuery(graphql` query { @@ -82,30 +81,10 @@ const Image = ({ } } } - - getAllImages: allImageSharp { - edges { - node { - fixed(height: 50) { - ...GatsbyImageSharpFixed - originalName - } - } - } - } } `); - const filter = data.getAllImages.edges.filter( - (n) => n.node.fixed.originalName === imageName, - ); - const imgStyle = width && height ? { width, height } : {}; - - return type === 'db' ? ( - - ) : ( - - ); + return ; }; export default Image; diff --git a/docs/src/components/layout.tsx b/docs/src/components/layout.tsx index 752578c550b09..a5a3fe12208fd 100644 --- a/docs/src/components/layout.tsx +++ b/docs/src/components/layout.tsx @@ -74,7 +74,7 @@ const sidebarStyle = css` border-right: 1px solid #bfbfbf; `; -const contentStyle = css` +const doczLayoutStyle = css` margin-top: 3px; background-color: white; img { @@ -124,6 +124,20 @@ const contentLayoutDocsStyle = css` overflow: auto; } `; +const footerHeight = 135; +const baseLayoutStyle = css` + min-height: 100vh; + position: relative; + .layout-footer { + position: absolute; + bottom: 0; + width: 100%; + height: ${footerHeight}px; + } + .content-wrap { + padding-bottom: ${footerHeight}px; + } +`; interface Props { children: React.ReactNode; @@ -153,7 +167,7 @@ const AppLayout = ({ children }: Props) => { - +

setDrawer(true)}> { ) : ( - - {children} -
+ +
+ {children} +
+
+
+
)} diff --git a/docs/src/images/apache-drill.png b/docs/src/images/databases/apache-drill.png similarity index 100% rename from docs/src/images/apache-drill.png rename to docs/src/images/databases/apache-drill.png diff --git a/docs/src/images/apache-druid.jpeg b/docs/src/images/databases/apache-druid.jpeg similarity index 100% rename from docs/src/images/apache-druid.jpeg rename to docs/src/images/databases/apache-druid.jpeg diff --git a/docs/src/images/apache-druid.png b/docs/src/images/databases/apache-druid.png similarity index 100% rename from docs/src/images/apache-druid.png rename to docs/src/images/databases/apache-druid.png diff --git a/docs/src/images/apache-hive.svg b/docs/src/images/databases/apache-hive.svg similarity index 100% rename from docs/src/images/apache-hive.svg rename to docs/src/images/databases/apache-hive.svg diff --git a/docs/src/images/apache-impala.png b/docs/src/images/databases/apache-impala.png similarity index 100% rename from docs/src/images/apache-impala.png rename to docs/src/images/databases/apache-impala.png diff --git a/docs/src/images/apache-kylin.png b/docs/src/images/databases/apache-kylin.png similarity index 100% rename from docs/src/images/apache-kylin.png rename to docs/src/images/databases/apache-kylin.png diff --git a/docs/src/images/aws-redshift.png b/docs/src/images/databases/aws-redshift.png similarity index 100% rename from docs/src/images/aws-redshift.png rename to docs/src/images/databases/aws-redshift.png diff --git a/docs/src/images/clickhouse.png b/docs/src/images/databases/clickhouse.png similarity index 100% rename from docs/src/images/clickhouse.png rename to docs/src/images/databases/clickhouse.png diff --git a/docs/src/images/druid.png b/docs/src/images/databases/druid.png similarity index 100% rename from docs/src/images/druid.png rename to docs/src/images/databases/druid.png diff --git a/docs/src/images/exasol.png b/docs/src/images/databases/exasol.png similarity index 100% rename from docs/src/images/exasol.png rename to docs/src/images/databases/exasol.png diff --git a/docs/src/images/firebird.png b/docs/src/images/databases/firebird.png similarity index 100% rename from docs/src/images/firebird.png rename to docs/src/images/databases/firebird.png diff --git a/docs/src/images/googleBQ.png b/docs/src/images/databases/googleBQ.png similarity index 100% rename from docs/src/images/googleBQ.png rename to docs/src/images/databases/googleBQ.png diff --git a/docs/src/images/greenplum.jpeg b/docs/src/images/databases/greenplum.jpeg similarity index 100% rename from docs/src/images/greenplum.jpeg rename to docs/src/images/databases/greenplum.jpeg diff --git a/docs/src/images/greenplum.png b/docs/src/images/databases/greenplum.png similarity index 100% rename from docs/src/images/greenplum.png rename to docs/src/images/databases/greenplum.png diff --git a/docs/src/images/ibmdb2.png b/docs/src/images/databases/ibmdb2.png similarity index 100% rename from docs/src/images/ibmdb2.png rename to docs/src/images/databases/ibmdb2.png diff --git a/docs/src/images/monet.png b/docs/src/images/databases/monet.png similarity index 100% rename from docs/src/images/monet.png rename to docs/src/images/databases/monet.png diff --git a/docs/src/images/msql.png b/docs/src/images/databases/msql.png similarity index 100% rename from docs/src/images/msql.png rename to docs/src/images/databases/msql.png diff --git a/docs/src/images/mysql.png b/docs/src/images/databases/mysql.png similarity index 100% rename from docs/src/images/mysql.png rename to docs/src/images/databases/mysql.png diff --git a/docs/src/images/oracle-logo.png b/docs/src/images/databases/oracle-logo.png similarity index 100% rename from docs/src/images/oracle-logo.png rename to docs/src/images/databases/oracle-logo.png diff --git a/docs/src/images/oracle.png b/docs/src/images/databases/oracle.png similarity index 100% rename from docs/src/images/oracle.png rename to docs/src/images/databases/oracle.png diff --git a/docs/src/images/oraclelogo.png b/docs/src/images/databases/oraclelogo.png similarity index 100% rename from docs/src/images/oraclelogo.png rename to docs/src/images/databases/oraclelogo.png diff --git a/docs/src/images/postgresql.jpg b/docs/src/images/databases/postgresql.jpg similarity index 100% rename from docs/src/images/postgresql.jpg rename to docs/src/images/databases/postgresql.jpg diff --git a/docs/src/images/postsql.png b/docs/src/images/databases/postsql.png similarity index 100% rename from docs/src/images/postsql.png rename to docs/src/images/databases/postsql.png diff --git a/docs/src/images/presto-og.png b/docs/src/images/databases/presto-og.png similarity index 100% rename from docs/src/images/presto-og.png rename to docs/src/images/databases/presto-og.png diff --git a/docs/src/images/snowflake.png b/docs/src/images/databases/snowflake.png similarity index 100% rename from docs/src/images/snowflake.png rename to docs/src/images/databases/snowflake.png diff --git a/docs/src/images/sqllite.jpg b/docs/src/images/databases/sqllite.jpg similarity index 100% rename from docs/src/images/sqllite.jpg rename to docs/src/images/databases/sqllite.jpg diff --git a/docs/src/images/sqllite.png b/docs/src/images/databases/sqllite.png similarity index 100% rename from docs/src/images/sqllite.png rename to docs/src/images/databases/sqllite.png diff --git a/docs/src/images/vertica.png b/docs/src/images/databases/vertica.png similarity index 100% rename from docs/src/images/vertica.png rename to docs/src/images/databases/vertica.png diff --git a/docs/src/images/gallery/bubble.png b/docs/src/images/gallery/bubble.png new file mode 100644 index 0000000000000..0d1f4c366a05b Binary files /dev/null and b/docs/src/images/gallery/bubble.png differ diff --git a/docs/src/images/gallery/chord_diagram.png b/docs/src/images/gallery/chord_diagram.png new file mode 100644 index 0000000000000..216db6ccb44e1 Binary files /dev/null and b/docs/src/images/gallery/chord_diagram.png differ diff --git a/docs/src/images/gallery/community.png b/docs/src/images/gallery/community.png new file mode 100644 index 0000000000000..9b2c6948765cb Binary files /dev/null and b/docs/src/images/gallery/community.png differ diff --git a/docs/src/images/gallery/dashboard_editor.png b/docs/src/images/gallery/dashboard_editor.png new file mode 100644 index 0000000000000..8d1b079280342 Binary files /dev/null and b/docs/src/images/gallery/dashboard_editor.png differ diff --git a/docs/src/images/gallery/dashboard_list.png b/docs/src/images/gallery/dashboard_list.png new file mode 100644 index 0000000000000..d72a48dd1cca7 Binary files /dev/null and b/docs/src/images/gallery/dashboard_list.png differ diff --git a/docs/src/images/gallery/dashboard_properties.png b/docs/src/images/gallery/dashboard_properties.png new file mode 100644 index 0000000000000..d05da04a02e88 Binary files /dev/null and b/docs/src/images/gallery/dashboard_properties.png differ diff --git a/docs/src/images/gallery/deck_arc.png b/docs/src/images/gallery/deck_arc.png new file mode 100644 index 0000000000000..d0b3e7e290fbe Binary files /dev/null and b/docs/src/images/gallery/deck_arc.png differ diff --git a/docs/src/images/gallery/deck_hex.png b/docs/src/images/gallery/deck_hex.png new file mode 100644 index 0000000000000..50e7088b3b21e Binary files /dev/null and b/docs/src/images/gallery/deck_hex.png differ diff --git a/docs/src/images/gallery/deck_path.png b/docs/src/images/gallery/deck_path.png new file mode 100644 index 0000000000000..a8c45012233b5 Binary files /dev/null and b/docs/src/images/gallery/deck_path.png differ diff --git a/docs/src/images/gallery/deck_polygon.png b/docs/src/images/gallery/deck_polygon.png new file mode 100644 index 0000000000000..f67f94b452b44 Binary files /dev/null and b/docs/src/images/gallery/deck_polygon.png differ diff --git a/docs/src/images/gallery/deck_scatter.png b/docs/src/images/gallery/deck_scatter.png new file mode 100644 index 0000000000000..7226226b90b21 Binary files /dev/null and b/docs/src/images/gallery/deck_scatter.png differ diff --git a/docs/src/images/gallery/deckgl_dash.png b/docs/src/images/gallery/deckgl_dash.png new file mode 100644 index 0000000000000..6ba049c7ae378 Binary files /dev/null and b/docs/src/images/gallery/deckgl_dash.png differ diff --git a/docs/src/images/gallery/explore.png b/docs/src/images/gallery/explore.png new file mode 100644 index 0000000000000..40c24c365af54 Binary files /dev/null and b/docs/src/images/gallery/explore.png differ diff --git a/docs/src/images/gallery/force_layout.png b/docs/src/images/gallery/force_layout.png new file mode 100644 index 0000000000000..be1966295795b Binary files /dev/null and b/docs/src/images/gallery/force_layout.png differ diff --git a/docs/src/images/gallery/france.png b/docs/src/images/gallery/france.png new file mode 100644 index 0000000000000..bd8572fe904cd Binary files /dev/null and b/docs/src/images/gallery/france.png differ diff --git a/docs/src/images/gallery/girl_names.png b/docs/src/images/gallery/girl_names.png new file mode 100644 index 0000000000000..d685b19be3b0c Binary files /dev/null and b/docs/src/images/gallery/girl_names.png differ diff --git a/docs/src/images/gallery/heatmap.png b/docs/src/images/gallery/heatmap.png new file mode 100644 index 0000000000000..69f5b365eb34f Binary files /dev/null and b/docs/src/images/gallery/heatmap.png differ diff --git a/docs/src/images/gallery/pino_geo.png b/docs/src/images/gallery/pino_geo.png new file mode 100644 index 0000000000000..f4288c5006ea9 Binary files /dev/null and b/docs/src/images/gallery/pino_geo.png differ diff --git a/docs/src/images/gallery/sankey.png b/docs/src/images/gallery/sankey.png new file mode 100644 index 0000000000000..38719ec326d23 Binary files /dev/null and b/docs/src/images/gallery/sankey.png differ diff --git a/docs/src/images/gallery/slack.png b/docs/src/images/gallery/slack.png new file mode 100644 index 0000000000000..c8904bfb1facf Binary files /dev/null and b/docs/src/images/gallery/slack.png differ diff --git a/docs/src/images/gallery/sqllab.png b/docs/src/images/gallery/sqllab.png new file mode 100644 index 0000000000000..ee59c37a7b181 Binary files /dev/null and b/docs/src/images/gallery/sqllab.png differ diff --git a/docs/src/images/gallery/storm.png b/docs/src/images/gallery/storm.png new file mode 100644 index 0000000000000..aa52b4bfb010f Binary files /dev/null and b/docs/src/images/gallery/storm.png differ diff --git a/docs/src/images/gallery/stream.png b/docs/src/images/gallery/stream.png new file mode 100644 index 0000000000000..2b10d68aebefe Binary files /dev/null and b/docs/src/images/gallery/stream.png differ diff --git a/docs/src/images/gallery/table.png b/docs/src/images/gallery/table.png new file mode 100644 index 0000000000000..7d1e6a7385425 Binary files /dev/null and b/docs/src/images/gallery/table.png differ diff --git a/docs/src/images/gallery/treemap.png b/docs/src/images/gallery/treemap.png new file mode 100644 index 0000000000000..9bf2e2b020cab Binary files /dev/null and b/docs/src/images/gallery/treemap.png differ diff --git a/docs/src/images/gallery/visualizations.png b/docs/src/images/gallery/visualizations.png new file mode 100644 index 0000000000000..995ca9c0869c2 Binary files /dev/null and b/docs/src/images/gallery/visualizations.png differ diff --git a/docs/src/images/gallery/worldbank_dashboard.png b/docs/src/images/gallery/worldbank_dashboard.png new file mode 100644 index 0000000000000..d8e91b8f4941c Binary files /dev/null and b/docs/src/images/gallery/worldbank_dashboard.png differ diff --git a/docs/src/images/gatsby-astronaut.png b/docs/src/images/gatsby-astronaut.png deleted file mode 100644 index da58ece0a8c5b..0000000000000 Binary files a/docs/src/images/gatsby-astronaut.png and /dev/null differ diff --git a/docs/src/images/gatsby-icon.png b/docs/src/images/gatsby-icon.png deleted file mode 100644 index 908bc78a7f559..0000000000000 Binary files a/docs/src/images/gatsby-icon.png and /dev/null differ diff --git a/docs/src/images/mysql.html b/docs/src/images/mysql.html deleted file mode 100644 index b1e75bcf206a2..0000000000000 --- a/docs/src/images/mysql.html +++ /dev/null @@ -1,1062 +0,0 @@ - - - - - - - - Mysql Logo png download - 1064*796 - Free Transparent Mysql png Download. - - CleanPNG / KissPNG - - - - - - - - - - -
-
-
- - - Upload - - -
-
- -
-
-
-
- -
-
-
-
- PNG Download - / - Others -
-
-

Mysql Logo

-
-
-
-
-
-
- -
- -
- Preview -
-
-
-
- Contributor: - Jocleyn - Send a message -
-
- Resolution: - 1064*796 -
-
- Size: - 8.26 KB -
-
- -
- - -
- -
-
-
- - - - -
-
-
-
-
-
-
-
-
-
Similar Images
-
-
    -
  • -
    -
    - Sql Logo - sql logo - -
    -
    -
  • -
  • -
    -
    - Fish Cartoon - frie - -
    -
    -
  • -
  • -
    -
    - Sql Icon - Trend Importexport - -
    -
    -
  • -
  • -
    -
    - Sql Server Logo - others - -
    -
    -
  • -
  • -
    -
    - FIREBIRD - -
    -
    -
  • -
  • -
    -
    - Sql Logo - mysql png clipart - -
    -
    -
  • -
  • -
    -
    - Ajax Logo - Carnifex - -
    -
    -
  • -
  • -
    -
    - Mysql Logo - WordPress - -
    -
    -
  • -
  • -
    -
    - Sql Server Logo - -
    -
    -
  • -
  • -
    -
    - Mysql Logo - others - -
    -
    -
  • -
  • -
    -
    - Mysql Logo - database Server - -
    -
    -
  • -
  • -
    -
    - Data Privacy Day - -
    -
    -
  • -
  • -
    -
    - update button - -
    -
    -
  • -
  • -
    -
    - Fedora - back - -
    -
    -
  • -
  • -
    -
    - Table Cartoon - olive in ant farm - -
    -
    -
  • -
  • -
    -
    - Sql Server Logo - Laxyo Solution Soft Pvt Ltd - -
    -
    -
  • -
  • -
    -
    - Sql Logo - table - -
    -
    -
  • -
  • -
    -
    - Mysql Logo - oracle sql logo - -
    -
    -
  • -
  • -
    -
    - Sql Logo - table - -
    -
    -
  • -
  • -
    -
    - Mysql Logo - technology web design - -
    -
    -
  • -
-
-
-
-
-
-
-
- -
-
- -
-
Trending Searches
- -
-
-
- -
-
-
-
- Send a message - -
-
- Contributor: - -
-
- Image: - -
-
-
- Your Email: - -
-
- Your Name: - -
-
- Message: - -
-
- Code: - - -
-
-
- - Send - -
-
-
- - - - - - - - - diff --git a/docs/src/pages/404.jsx b/docs/src/pages/404.jsx index 325d51aa61be2..afbf936b3f7f7 100644 --- a/docs/src/pages/404.jsx +++ b/docs/src/pages/404.jsx @@ -25,7 +25,7 @@ const NotFoundPage = () => (

NOT FOUND

-

You just hit a route that does not exist... the sadness.

+

Sorry, you've requested a page that does not exist.

); diff --git a/docs/src/pages/docs/contributing-page.mdx b/docs/src/pages/docs/contributing-page.mdx index 0f8c83fa9bf64..e07cc7de4d681 100644 --- a/docs/src/pages/docs/contributing-page.mdx +++ b/docs/src/pages/docs/contributing-page.mdx @@ -11,7 +11,7 @@ Superset is currently being incubated at contributors (or committers) to Superset communicate primarily in the following channels (all of which you can join): -- [mailing list](https://lists.apache.org/list.html?dev@superset.apache.org) +- [Mailing list](https://lists.apache.org/list.html?dev@superset.apache.org) - [Apache Superset Slack community](https://apache-superset.slack.com/join/shared_invite/zt-g8lpruog-HeqpgYrwdfrD5OYhlU7hPQ#/) - [Github issues and PR's](https://github.com/apache/incubator-superset/issues) diff --git a/docs/src/pages/gallery.tsx b/docs/src/pages/gallery.tsx new file mode 100644 index 0000000000000..57e1cc27a838f --- /dev/null +++ b/docs/src/pages/gallery.tsx @@ -0,0 +1,110 @@ +/** + * 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 React from 'react'; +import { css } from '@emotion/core'; +import { useStaticQuery, graphql } from 'gatsby'; +import Gallery from 'react-grid-gallery'; +import Layout from '../components/layout'; + +const galleryStyle = css` + margin-bottom: 25px; + padding-top: 100px; + padding-left: 50px; + padding-right: 50px; + text-align: center; + .ReactGridGallery_tile-viewport { + overflow: visible !important; + } + .ReactGridGallery img { + box-shadow: 0px 0px 3px 1px #AAA; + } +`; + +// This defines the ordering of the images in the gallery +// and allows to add metadata to images. +const imageMeta = { + 'worldbank_dashboard.png': { caption: "World's Bank Dashboard" }, + 'sqllab.png': { caption: 'SQL Lab' }, + 'explore.png': { caption: 'Explore!' }, + 'visualizations.png': { caption: 'Visualizations' }, + 'chord_diagram.png': { caption: 'Explore' }, + 'deck_scatter.png': { caption: 'Geospatial Scatterplot' }, + 'deck_polygon.png': { caption: 'Geospatial Polygon' }, + 'deck_arc.png': { caption: 'Geospatial Arc' }, + 'deck_path.png': { caption: 'Geospatial Path' }, +}; + +const GalleryPage = () => { + const data = useStaticQuery(graphql` + query { + allImages: allFile(filter: {extension: {eq: "png"}, relativeDirectory: {regex: "/gallery/"}}) { + edges { + node { + thumb: childImageSharp { + fixed(height: 350) { + ...GatsbyImageSharpFixed + originalName + } + } + full: childImageSharp { + fixed(height: 1600) { + ...GatsbyImageSharpFixed + originalName + } + } + } + } + } + } + `); + const imagesMap = {}; + data.allImages.edges.map((img) => img.node).forEach((img) => { + imagesMap[img.thumb.fixed.originalName] = { + src: img.full.fixed.src, + thumbnail: img.thumb.fixed.src, + // caption: img.thumb.fixed.originalName, + }; + }); + + const augmentedImages = []; + Object.keys(imageMeta).forEach((originalName) => { + const img = imagesMap[originalName]; + delete imagesMap[originalName]; + augmentedImages.push({ + ...img, + ...imageMeta[originalName], + }); + }); + Object.values(imagesMap).forEach((img) => { + augmentedImages.push(img); + }); + return ( + +
+ +
+
+ ); +}; +export default GalleryPage; diff --git a/docs/src/pages/index.tsx b/docs/src/pages/index.tsx index 5b6099686776c..a63342062be26 100644 --- a/docs/src/pages/index.tsx +++ b/docs/src/pages/index.tsx @@ -36,6 +36,7 @@ import GitHubButton from 'react-github-btn'; import { Databases } from '../resources/data'; import Layout from '../components/layout'; import Image from '../components/image'; +import DbImage from '../components/DbImage'; import 'antd/dist/antd.css'; import SEO from '../components/seo'; import logo from '../images/superset-logo-horiz-apache.svg'; @@ -179,7 +180,7 @@ const integrationSection = css` font-size: 18px; } - .databaseList { + .database-list { margin-top: 100px; list-style-type: none; padding: 0px; @@ -448,19 +449,20 @@ const Theme = () => {

Supported Databases

-

diff --git a/superset-frontend/.eslintrc.js b/superset-frontend/.eslintrc.js index 0a3b7aa996e9b..1d5c49220d77b 100644 --- a/superset-frontend/.eslintrc.js +++ b/superset-frontend/.eslintrc.js @@ -86,7 +86,6 @@ module.exports = { '.json': 'always', }, ], - 'import/no-named-as-default': 0, 'import/no-named-as-default-member': 0, 'import/prefer-default-export': 0, indent: 0, @@ -101,7 +100,6 @@ module.exports = { 'no-multi-spaces': 0, 'no-prototype-builtins': 0, 'no-restricted-properties': 0, - 'no-restricted-syntax': 0, 'no-restricted-imports': [ 'error', { @@ -126,7 +124,6 @@ module.exports = { 'react/jsx-fragments': 1, 'react/jsx-no-bind': 0, 'react/jsx-props-no-spreading': 0, // re-enable up for discussion - 'react/no-access-state-in-setstate': 0, // disabled temporarily 'react/no-array-index-key': 0, 'react/no-string-refs': 0, 'react/no-unescaped-entities': 0, @@ -201,7 +198,6 @@ module.exports = { }, ], 'import/no-cycle': 0, // re-enable up for discussion, might require some major refactors - 'import/no-named-as-default': 0, 'import/prefer-default-export': 0, indent: 0, 'jsx-a11y/anchor-is-valid': 0, // disabled temporarily @@ -215,7 +211,6 @@ module.exports = { 'no-multi-spaces': 0, 'no-prototype-builtins': 0, 'no-restricted-properties': 0, - 'no-restricted-syntax': 0, 'no-restricted-imports': [ 'error', { @@ -240,7 +235,6 @@ module.exports = { 'react/jsx-fragments': 1, 'react/jsx-no-bind': 0, 'react/jsx-props-no-spreading': 0, // re-enable up for discussion - 'react/no-access-state-in-setstate': 0, // disabled temporarily 'react/no-array-index-key': 0, 'react/no-string-refs': 0, 'react/no-unescaped-entities': 0, diff --git a/superset-frontend/cypress-base/cypress/integration/sqllab/tabs.test.js b/superset-frontend/cypress-base/cypress/integration/sqllab/tabs.test.js index 49629f443a6b7..b598cb8d8f112 100644 --- a/superset-frontend/cypress-base/cypress/integration/sqllab/tabs.test.js +++ b/superset-frontend/cypress-base/cypress/integration/sqllab/tabs.test.js @@ -24,33 +24,39 @@ describe('SqlLab query tabs', () => { }); it('allows you to create a tab', () => { - cy.get('.SqlEditorTabs > ul > li').then(tabList => { + cy.get('[data-test="sql-editor-tabs"]').then(tabList => { const initialTabCount = tabList.length; // add tab - cy.get('.SqlEditorTabs > ul > li').last().click(); + cy.get('[data-test="add-tab-icon"]').click(); // wait until we find the new tab - cy.get(`.SqlEditorTabs > ul > li:eq(${initialTabCount - 1})`).contains( - 'Untitled Query', - ); + cy.get('[data-test="sql-editor-tabs"]') + .children() + .eq(initialTabCount - 1) + .contains(`Untitled Query ${initialTabCount + 1}`); + cy.get('[data-test="sql-editor-tabs"]') + .children() + .eq(initialTabCount) + .contains(`Untitled Query ${initialTabCount + 2}`); }); }); - it('allows you to close a tab', () => { - cy.get('.SqlEditorTabs > ul > li').then(tabListA => { - const initialTabCount = tabListA.length; + cy.get('[data-test="sql-editor-tabs"]') + .children() + .then(tabListA => { + const initialTabCount = tabListA.length; - // open the tab dropdown to remove - cy.get('.SqlEditorTabs > ul > li .dropdown-toggle').click({ - force: true, - }); + // open the tab dropdown to remove + cy.get('[data-test="dropdown-toggle-button"]').click({ + force: true, + }); - // first item is close - cy.get('.SqlEditorTabs .ddbtn-tab svg').first().click(); + // first item is close + cy.get('[data-test="close-tab-menu-option"]').click(); - cy.get('.SqlEditorTabs > ul > li').should( - 'have.length', - initialTabCount - 1, - ); - }); + cy.get('[data-test="sql-editor-tabs"]').should( + 'have.length', + initialTabCount - 1, + ); + }); }); }); diff --git a/superset-frontend/images/screenshots/gallery.png b/superset-frontend/images/screenshots/gallery.png new file mode 100644 index 0000000000000..99ef8149c41b3 Binary files /dev/null and b/superset-frontend/images/screenshots/gallery.png differ diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 0a8b674d0949b..4c73a398a4055 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -3399,6 +3399,11 @@ "inline-style-prefixer": "^3.0.1", "string-hash": "^1.1.3" } + }, + "immutable": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", + "integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=" } } }, @@ -15494,6 +15499,13 @@ "react-map-gl": "^4.0.10", "supercluster": "^4.1.1", "viewport-mercator-project": "^6.1.1" + }, + "dependencies": { + "immutable": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", + "integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=" + } } }, "@superset-ui/legacy-plugin-chart-paired-t-test": { @@ -17257,6 +17269,12 @@ "@types/sizzle": "*" } }, + "@types/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-WW+0cfH3ovFN6ROV+p/Xfw36dT6s16hbXBYIG49PYw6+j6e+AkpqYccctgxwyicBmC8CZDBnPhOH94shFhXgHQ==", + "dev": true + }, "@types/json-schema": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz", @@ -20306,9 +20324,9 @@ "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==" }, "bignumber.js": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz", - "integrity": "sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ==" + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", + "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==" }, "binary-extensions": { "version": "1.12.0", @@ -27635,9 +27653,9 @@ "dev": true }, "highlight.js": { - "version": "9.12.0", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.12.0.tgz", - "integrity": "sha1-5tnb5Xy+/mB1HwKvM2GVhwyQwB4=" + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.2.0.tgz", + "integrity": "sha512-OryzPiqqNCfO/wtFo619W+nPYALM6u7iCQkum4bqRmmlcTikOkmlL06i009QelynBPAlNByTQU6cBB2cOBQtCw==" }, "history": { "version": "4.10.1", @@ -28094,9 +28112,9 @@ "dev": true }, "immutable": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", - "integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=" + "version": "4.0.0-rc.12", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0-rc.12.tgz", + "integrity": "sha512-0M2XxkZLx/mi3t8NVwIm1g8nHoEmM9p9UBl/G9k4+hm0kBgOVdMV/B3CY5dQ8qG8qc80NN4gDV4HQv6FTJ5q7A==" }, "import-cwd": { "version": "2.1.0", @@ -32799,11 +32817,11 @@ "dev": true }, "json-bigint": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-0.3.0.tgz", - "integrity": "sha1-DM2RLEuCcNBfBW+9E4FLU9OCWx4=", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", "requires": { - "bignumber.js": "^7.0.0" + "bignumber.js": "^9.0.0" } }, "json-parse-better-errors": { @@ -33417,12 +33435,19 @@ } }, "lowlight": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.9.2.tgz", - "integrity": "sha512-Ek18ElVCf/wF/jEm1b92gTnigh94CtBNWiZ2ad+vTgW7cTmQxUY3I98BjHK68gZAJEWmybGBZgx9qv3QxLQB/Q==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.14.0.tgz", + "integrity": "sha512-N2E7zTM7r1CwbzwspPxJvmjAbxljCPThTFawEX2Z7+P3NGrrvY54u8kyU16IY4qWfoVIxY8SYCS8jTkuG7TqYA==", "requires": { - "fault": "^1.0.2", - "highlight.js": "~9.12.0" + "fault": "^1.0.0", + "highlight.js": "~10.1.0" + }, + "dependencies": { + "highlight.js": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.1.2.tgz", + "integrity": "sha512-Q39v/Mn5mfBlMff9r+zzA+gWxRsCRKwEMvYTiisLr/XUiFI/4puWt0Ojdko3R3JCNWGdOWaA5g/Yxqa23kC5AA==" + } } }, "lru-cache": { @@ -37777,6 +37802,7 @@ "version": "1.15.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.15.0.tgz", "integrity": "sha512-Lf2JrFYx8FanHrjoV5oL8YHCclLQgbJcVZR+gikGGMqz6ub5QVWDTM6YIwm3BuPxM/LOV+rKns3LssXNLIf+DA==", + "dev": true, "requires": { "clipboard": "^2.0.0" } @@ -39048,9 +39074,9 @@ } }, "react-bootstrap-dialog": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/react-bootstrap-dialog/-/react-bootstrap-dialog-0.10.0.tgz", - "integrity": "sha1-/KXISATqK23r44M8bUt0gLz/AXU=" + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/react-bootstrap-dialog/-/react-bootstrap-dialog-0.13.0.tgz", + "integrity": "sha512-mchodv4gJKFINq87OAJR8E3fpOHPyq3BY1xg3DUSH64j7T38DK0aOOK/zOcLuBxIEy9Wc2QWsjJqILXc8eWkoQ==" }, "react-bootstrap-slider": { "version": "2.1.5", @@ -40311,9 +40337,9 @@ "integrity": "sha512-p84kBqGaMoa7VYT0vZ/aOYRfJB+gw34yjpda1Z5KeLflg70HipZOT+MXQenEhdkPAABuE2Astq4zEPdMqUQxcg==" }, "react-overlays": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-0.9.1.tgz", - "integrity": "sha512-b0asy/zHtRd0i2+2/uNxe3YVprF3bRT1guyr791DORjCzE/HSBMog+ul83CdtKQ1kZ+pLnxWCu5W3BMysFhHdQ==", + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-0.9.2.tgz", + "integrity": "sha512-wOi+WqO0acnUAMCbTTW06/GRkYjHdlvIoyX4bYkLvxKrLgl2kX9WzFVyBdwukl2jvN7I7oX7ZXAz7MNOWYdgCA==", "requires": { "classnames": "^2.2.5", "dom-helpers": "^3.2.1", @@ -40580,15 +40606,56 @@ } }, "react-syntax-highlighter": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-7.0.4.tgz", - "integrity": "sha512-WtaHAlI5++csZ5uTnJc5+ozqqIzUkO/rnkv1GJ3CeRtjhTzbo12r9F0BICzhibr7gBWECd1Xgj1FKJEWZxcP4w==", + "version": "13.5.3", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-13.5.3.tgz", + "integrity": "sha512-crPaF+QGPeHNIblxxCdf2Lg936NAHKhNhuMzRL3F9ct6aYXL3NcZtCL0Rms9+qVo6Y1EQLdXGypBNSbPL/r+qg==", "requires": { - "babel-runtime": "^6.18.0", - "highlight.js": "~9.12.0", - "lowlight": "~1.9.1", - "prismjs": "^1.8.4", - "refractor": "^2.4.1" + "@babel/runtime": "^7.3.1", + "highlight.js": "^10.1.1", + "lowlight": "^1.14.0", + "prismjs": "^1.21.0", + "refractor": "^3.1.0" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.11.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz", + "integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "requires": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + } + }, + "prismjs": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.21.0.tgz", + "integrity": "sha512-uGdSIu1nk3kej2iZsLyDoJ7e9bnPzIgY0naW/HdknGj61zScaprVEVGHrPoXqI+M9sP0NDnTK2jpkvmldpuqDw==", + "requires": { + "clipboard": "^2.0.0" + } + }, + "refractor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.1.0.tgz", + "integrity": "sha512-bN8GvY6hpeXfC4SzWmYNQGLLF2ZakRDNBkgCL0vvl5hnpMrnyURk8Mv61v6pzn4/RBHzSWLp44SzMmVHqMGNww==", + "requires": { + "hastscript": "^5.0.0", + "parse-entities": "^2.0.0", + "prismjs": "~1.21.0" + } + } } }, "react-table": { @@ -41041,6 +41108,7 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/refractor/-/refractor-2.6.2.tgz", "integrity": "sha512-AMNEGkhaXfhoa0/0mW0bHdfizDJnuHDK29/D5oQaKICf6DALQ+kDEHW/36oDHCdfva4XrZ+cdMhRvPsTI4OIjA==", + "dev": true, "requires": { "hastscript": "^5.0.0", "parse-entities": "^1.1.2", @@ -45266,9 +45334,9 @@ }, "dependencies": { "@babel/runtime": { - "version": "7.9.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.2.tgz", - "integrity": "sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q==", + "version": "7.11.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz", + "integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==", "requires": { "regenerator-runtime": "^0.13.4" } diff --git a/superset-frontend/package.json b/superset-frontend/package.json index f8f098012aeb5..786734a8e5878 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -108,10 +108,10 @@ "dom-to-image": "^2.6.0", "geolib": "^2.0.24", "global-box": "^1.2.0", - "immutable": "^3.8.2", + "immutable": "^4.0.0-rc.12", "interweave": "^11.2.0", "jquery": "^3.5.1", - "json-bigint": "^0.3.0", + "json-bigint": "^1.0.0", "lodash": "^4.17.20", "lodash-es": "^4.17.14", "mathjs": "^3.20.2", @@ -127,7 +127,7 @@ "react-ace": "^5.10.0", "react-avatar": "^3.9.7", "react-bootstrap": "^0.33.1", - "react-bootstrap-dialog": "^0.10.0", + "react-bootstrap-dialog": "^0.13.0", "react-bootstrap-slider": "2.1.5", "react-checkbox-tree": "^1.5.1", "react-color": "^2.13.8", @@ -150,7 +150,7 @@ "react-sortable-hoc": "^1.11.0", "react-split": "^2.0.4", "react-sticky": "^6.0.2", - "react-syntax-highlighter": "^7.0.4", + "react-syntax-highlighter": "^13.5.3", "react-table": "^7.2.1", "react-transition-group": "^2.5.3", "react-ultimate-pagination": "^1.2.0", @@ -200,6 +200,7 @@ "@types/fetch-mock": "^7.3.2", "@types/jest": "^26.0.3", "@types/jquery": "^3.3.32", + "@types/json-bigint": "^1.0.0", "@types/react": "^16.9.43", "@types/react-bootstrap": "^0.32.22", "@types/react-dom": "^16.9.8", diff --git a/superset-frontend/spec/javascripts/explore/components/CheckboxControl_spec.jsx b/superset-frontend/spec/javascripts/explore/components/CheckboxControl_spec.jsx index 9a0740cb01a84..9dc24811f6b31 100644 --- a/superset-frontend/spec/javascripts/explore/components/CheckboxControl_spec.jsx +++ b/superset-frontend/spec/javascripts/explore/components/CheckboxControl_spec.jsx @@ -21,6 +21,8 @@ import React from 'react'; import sinon from 'sinon'; import { shallow, mount } from 'enzyme'; +import { supersetTheme, ThemeProvider } from '@superset-ui/core'; + import CheckboxControl from 'src/explore/components/controls/CheckboxControl'; import ControlHeader from 'src/explore/components/ControlHeader'; import Checkbox from 'src/components/Checkbox'; @@ -48,7 +50,10 @@ describe('CheckboxControl', () => { }); it('Checks the box when the label is clicked', () => { - const fullComponent = mount(); + const fullComponent = mount(, { + wrappingComponent: ThemeProvider, + wrappingComponentProps: { theme: supersetTheme }, + }); const spy = sinon.spy(fullComponent.instance(), 'onChange'); diff --git a/superset-frontend/spec/javascripts/explore/components/DateFilterControl_spec.jsx b/superset-frontend/spec/javascripts/explore/components/DateFilterControl_spec.jsx index 693c86950cf1a..065f979539072 100644 --- a/superset-frontend/spec/javascripts/explore/components/DateFilterControl_spec.jsx +++ b/superset-frontend/spec/javascripts/explore/components/DateFilterControl_spec.jsx @@ -18,14 +18,21 @@ */ /* eslint-disable no-unused-expressions */ import React from 'react'; +import { OverlayTrigger, Popover, Tab, Tabs, Radio } from 'react-bootstrap'; import sinon from 'sinon'; import { styledMount as mount } from 'spec/helpers/theming'; -import Button from 'src/components/Button'; import Label from 'src/components/Label'; import DateFilterControl from 'src/explore/components/controls/DateFilterControl'; import ControlHeader from 'src/explore/components/ControlHeader'; +// Mock moment.js to use a specific date +jest.mock('moment', () => { + const testDate = new Date('09/07/2020'); + + return () => jest.requireActual('moment')(testDate); +}); + const defaultProps = { animation: false, name: 'date', @@ -41,40 +48,94 @@ describe('DateFilterControl', () => { wrapper = mount(); }); + it('renders', () => { + expect(wrapper.find(DateFilterControl)).toExist(); + }); + it('renders a ControlHeader', () => { const controlHeader = wrapper.find(ControlHeader); expect(controlHeader).toHaveLength(1); }); - it('renders 3 Buttons', () => { - const label = wrapper.find(Label).first(); - label.simulate('click'); - setTimeout(() => { - expect(wrapper.find(Button)).toHaveLength(3); - }, 10); + + it('renders an OverlayTrigger', () => { + expect(wrapper.find(OverlayTrigger)).toExist(); }); - it('loads the right state', () => { - const label = wrapper.find(Label).first(); - label.simulate('click'); - setTimeout(() => { - expect(wrapper.state().num).toBe('90'); - }, 10); + + it('renders a popover', () => { + const { overlay } = wrapper.find(OverlayTrigger).first().props(); + const overlayWrapper = mount(overlay); + + expect(overlayWrapper.find(Popover)).toExist(); }); - it('renders 2 dimmed sections', () => { - const label = wrapper.find(Label).first(); + + it('calls open/close methods on trigger click', () => { + const open = jest.fn(); + const close = jest.fn(); + const props = { + ...defaultProps, + onOpenDateFilterControl: open, + onCloseDateFilterControl: close, + }; + const testWrapper = mount(); + const label = testWrapper.find(Label).first(); + label.simulate('click'); - setTimeout(() => { - expect(wrapper.find(Button)).toHaveLength(3); - }, 10); - }); - it('opens and closes', () => { - const label = wrapper.find(Label).first(); + expect(open).toBeCalled(); + expect(close).not.toBeCalled(); label.simulate('click'); - setTimeout(() => { - expect(wrapper.find('.popover')).toExist(); - expect(wrapper.find('.ok')).first().simulate('click'); - setTimeout(() => { - expect(wrapper.find('.popover')).not.toExist(); - }, 10); - }, 10); + expect(close).toBeCalled(); + }); + + it('renders two tabs in popover', () => { + const { overlay } = wrapper.find(OverlayTrigger).first().props(); + const overlayWrapper = mount(overlay); + const popover = overlayWrapper.find(Popover).first(); + + expect(popover.find(Tabs)).toExist(); + expect(popover.find(Tab)).toHaveLength(2); + }); + + it('renders default time options', () => { + const { overlay } = wrapper.find(OverlayTrigger).first().props(); + const overlayWrapper = mount(overlay); + const defaultTab = overlayWrapper.find(Tab).first(); + + expect(defaultTab.find(Radio)).toExist(); + expect(defaultTab.find(Radio)).toHaveLength(6); + }); + + it('renders tooltips over timeframe options', () => { + const { overlay } = wrapper.find(OverlayTrigger).first().props(); + const overlayWrapper = mount(overlay); + const defaultTab = overlayWrapper.find(Tab).first(); + const radioTrigger = defaultTab.find(OverlayTrigger); + + expect(radioTrigger).toExist(); + expect(radioTrigger).toHaveLength(6); + }); + + it('renders the correct time range in tooltip', () => { + const { overlay } = wrapper.find(OverlayTrigger).first().props(); + const overlayWrapper = mount(overlay); + const defaultTab = overlayWrapper.find(Tab).first(); + const triggers = defaultTab.find(OverlayTrigger); + + const expectedLabels = { + 'Last day': '2020-09-06 < col < 2020-09-07', + 'Last week': '2020-08-31 < col < 2020-09-07', + 'Last month': '2020-08-07 < col < 2020-09-07', + 'Last quarter': '2020-06-07 < col < 2020-09-07', + 'Last year': '2019-01-01 < col < 2020-01-01', + 'No filter': '-∞ < col < ∞', + }; + + triggers.forEach(trigger => { + const { props } = trigger.props().overlay; + const label = props.id.split('tooltip-')[1]; + + expect(trigger.props().overlay.props.children).toEqual( + expectedLabels[label], + ); + }); }); }); diff --git a/superset-frontend/spec/javascripts/explore/components/ExploreViewContainer_spec.jsx b/superset-frontend/spec/javascripts/explore/components/ExploreViewContainer_spec.jsx index aa33285cc5ac4..ffcd43b480030 100644 --- a/superset-frontend/spec/javascripts/explore/components/ExploreViewContainer_spec.jsx +++ b/superset-frontend/spec/javascripts/explore/components/ExploreViewContainer_spec.jsx @@ -25,7 +25,7 @@ import { shallow } from 'enzyme'; import getInitialState from 'src/explore/reducers/getInitialState'; import ExploreViewContainer from 'src/explore/components/ExploreViewContainer'; import QueryAndSaveBtns from 'src/explore/components/QueryAndSaveBtns'; -import ControlPanelsContainer from 'src/explore/components/ControlPanelsContainer'; +import ConnectedControlPanelsContainer from 'src/explore/components/ControlPanelsContainer'; import ChartContainer from 'src/explore/components/ExploreChartPanel'; import * as featureFlags from 'src/featureFlags'; @@ -72,7 +72,7 @@ describe('ExploreViewContainer', () => { }); it('renders ControlPanelsContainer', () => { - expect(wrapper.find(ControlPanelsContainer)).toExist(); + expect(wrapper.find(ConnectedControlPanelsContainer)).toExist(); }); it('renders ChartContainer', () => { diff --git a/superset-frontend/spec/javascripts/explore/controlUtils_spec.jsx b/superset-frontend/spec/javascripts/explore/controlUtils_spec.jsx index 02c683f77ab15..e52f70dd0fa38 100644 --- a/superset-frontend/spec/javascripts/explore/controlUtils_spec.jsx +++ b/superset-frontend/spec/javascripts/explore/controlUtils_spec.jsx @@ -17,19 +17,20 @@ * under the License. */ -import React from 'react'; -import { - getChartControlPanelRegistry, - ColumnOption, - t, -} from '@superset-ui/core'; +import { getChartControlPanelRegistry, t } from '@superset-ui/core'; import { getControlConfig, getControlState, getFormDataFromControls, applyMapStateToPropsToControl, getAllControlsState, + findControlItem, } from 'src/explore/controlUtils'; +import { + controlPanelSectionsChartOptions, + controlPanelSectionsChartOptionsOnlyColorScheme, + controlPanelSectionsChartOptionsTable, +} from 'spec/javascripts/explore/fixtures'; describe('controlUtils', () => { const state = { @@ -43,56 +44,10 @@ describe('controlUtils', () => { beforeAll(() => { getChartControlPanelRegistry() .registerValue('test-chart', { - controlPanelSections: [ - { - label: t('Chart Options'), - expanded: true, - controlSetRows: [ - [ - 'color_scheme', - { - name: 'rose_area_proportion', - config: { - type: 'CheckboxControl', - label: t('Use Area Proportions'), - description: t( - 'Check if the Rose Chart should use segment area instead of ' + - 'segment radius for proportioning', - ), - default: false, - renderTrigger: true, - }, - }, - ], - [ - { - name: 'stacked_style', - config: { - type: 'SelectControl', - label: t('Stacked Style'), - renderTrigger: true, - choices: [ - ['stack', 'stack'], - ['stream', 'stream'], - ['expand', 'expand'], - ], - default: 'stack', - description: '', - }, - }, - ], - ], - }, - ], + controlPanelSections: controlPanelSectionsChartOptions, }) .registerValue('test-chart-override', { - controlPanelSections: [ - { - label: t('Chart Options'), - expanded: true, - controlSetRows: [['color_scheme']], - }, - ], + controlPanelSections: controlPanelSectionsChartOptionsOnlyColorScheme, controlOverrides: { color_scheme: { label: t('My beautiful colors'), @@ -100,40 +55,7 @@ describe('controlUtils', () => { }, }) .registerValue('table', { - controlPanelSections: [ - { - label: t('Chart Options'), - expanded: true, - controlSetRows: [ - [ - 'metric', - 'metrics', - { - name: 'all_columns', - config: { - type: 'SelectControl', - queryField: 'columns', - multi: true, - label: t('Columns'), - default: [], - description: t('Columns to display'), - optionRenderer: c => , - valueRenderer: c => , - valueKey: 'column_name', - allowAll: true, - mapStateToProps: stateRef => ({ - options: stateRef.datasource - ? stateRef.datasource.columns - : [], - }), - commaChoosesOption: false, - freeForm: true, - }, - }, - ], - ], - }, - ], + controlPanelSections: controlPanelSectionsChartOptionsTable, }); }); @@ -287,4 +209,38 @@ describe('controlUtils', () => { }); }); }); + + describe('findControlItem', () => { + it('find control as a string', () => { + const controlItem = findControlItem( + controlPanelSectionsChartOptions, + 'color_scheme', + ); + expect(controlItem).toEqual('color_scheme'); + }); + + it('find control as a control object', () => { + let controlItem = findControlItem( + controlPanelSectionsChartOptions, + 'rose_area_proportion', + ); + expect(controlItem.name).toEqual('rose_area_proportion'); + expect(controlItem).toHaveProperty('config'); + + controlItem = findControlItem( + controlPanelSectionsChartOptions, + 'stacked_style', + ); + expect(controlItem.name).toEqual('stacked_style'); + expect(controlItem).toHaveProperty('config'); + }); + + it('returns null when key is not found', () => { + const controlItem = findControlItem( + controlPanelSectionsChartOptions, + 'non_existing_key', + ); + expect(controlItem).toBeNull(); + }); + }); }); diff --git a/superset-frontend/spec/javascripts/explore/fixtures.jsx b/superset-frontend/spec/javascripts/explore/fixtures.jsx new file mode 100644 index 0000000000000..a4340854e86c1 --- /dev/null +++ b/superset-frontend/spec/javascripts/explore/fixtures.jsx @@ -0,0 +1,104 @@ +/** + * 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 React from 'react'; +import { ColumnOption, t } from '@superset-ui/core'; + +export const controlPanelSectionsChartOptions = [ + { + label: t('Chart Options'), + expanded: true, + controlSetRows: [ + [ + 'color_scheme', + { + name: 'rose_area_proportion', + config: { + type: 'CheckboxControl', + label: t('Use Area Proportions'), + description: t( + 'Check if the Rose Chart should use segment area instead of ' + + 'segment radius for proportioning', + ), + default: false, + renderTrigger: true, + }, + }, + ], + [ + { + name: 'stacked_style', + config: { + type: 'SelectControl', + label: t('Stacked Style'), + renderTrigger: true, + choices: [ + ['stack', 'stack'], + ['stream', 'stream'], + ['expand', 'expand'], + ], + default: 'stack', + description: '', + }, + }, + ], + ], + }, +]; + +export const controlPanelSectionsChartOptionsOnlyColorScheme = [ + { + label: t('Chart Options'), + expanded: true, + controlSetRows: [['color_scheme']], + }, +]; + +export const controlPanelSectionsChartOptionsTable = [ + { + label: t('Chart Options'), + expanded: true, + controlSetRows: [ + [ + 'metric', + 'metrics', + { + name: 'all_columns', + config: { + type: 'SelectControl', + queryField: 'columns', + multi: true, + label: t('Columns'), + default: [], + description: t('Columns to display'), + optionRenderer: c => , + valueRenderer: c => , + valueKey: 'column_name', + allowAll: true, + mapStateToProps: stateRef => ({ + options: stateRef.datasource ? stateRef.datasource.columns : [], + }), + commaChoosesOption: false, + freeForm: true, + }, + }, + ], + ], + }, +]; diff --git a/superset-frontend/spec/javascripts/sqllab/SqlEditor_spec.jsx b/superset-frontend/spec/javascripts/sqllab/SqlEditor_spec.jsx index 2498441ebba4e..15559c7cf877c 100644 --- a/superset-frontend/spec/javascripts/sqllab/SqlEditor_spec.jsx +++ b/superset-frontend/spec/javascripts/sqllab/SqlEditor_spec.jsx @@ -25,7 +25,7 @@ import { } from 'src/SqlLab/constants'; import AceEditorWrapper from 'src/SqlLab/components/AceEditorWrapper'; import LimitControl from 'src/SqlLab/components/LimitControl'; -import SouthPane from 'src/SqlLab/components/SouthPane'; +import ConnectedSouthPane from 'src/SqlLab/components/SouthPane'; import SqlEditor from 'src/SqlLab/components/SqlEditor'; import SqlEditorLeftBar from 'src/SqlLab/components/SqlEditorLeftBar'; @@ -64,15 +64,15 @@ describe('SqlEditor', () => { const wrapper = shallow(); expect(wrapper.find(AceEditorWrapper)).toExist(); }); - it('render an SouthPane', () => { + it('render a SouthPane', () => { const wrapper = shallow(); - expect(wrapper.find(SouthPane)).toExist(); + expect(wrapper.find(ConnectedSouthPane)).toExist(); }); it('does not overflow the editor window', () => { const wrapper = shallow(); const totalSize = parseFloat(wrapper.find(AceEditorWrapper).props().height) + - wrapper.find(SouthPane).props().height + + wrapper.find(ConnectedSouthPane).props().height + SQL_TOOLBAR_HEIGHT + SQL_EDITOR_GUTTER_MARGIN * 2 + SQL_EDITOR_GUTTER_HEIGHT; @@ -83,7 +83,7 @@ describe('SqlEditor', () => { wrapper.setState({ height: 450 }); const totalSize = parseFloat(wrapper.find(AceEditorWrapper).props().height) + - wrapper.find(SouthPane).props().height + + wrapper.find(ConnectedSouthPane).props().height + SQL_TOOLBAR_HEIGHT + SQL_EDITOR_GUTTER_MARGIN * 2 + SQL_EDITOR_GUTTER_HEIGHT; diff --git a/superset-frontend/src/CRUD/CollectionTable.tsx b/superset-frontend/src/CRUD/CollectionTable.tsx index f1620f43e9563..4e918665ba099 100644 --- a/superset-frontend/src/CRUD/CollectionTable.tsx +++ b/superset-frontend/src/CRUD/CollectionTable.tsx @@ -150,12 +150,12 @@ export default class CRUDCollection extends React.PureComponent< toggleExpand(id: any) { this.onCellChange(id, '__expanded', false); - this.setState({ + this.setState(prevState => ({ expandedColumns: { - ...this.state.expandedColumns, - [id]: !this.state.expandedColumns[id], + ...prevState.expandedColumns, + [id]: !prevState.expandedColumns[id], }, - }); + })); } renderHeaderRow() { diff --git a/superset-frontend/src/SqlLab/components/App.jsx b/superset-frontend/src/SqlLab/components/App.jsx index f10c3f629f69c..5934542611fb3 100644 --- a/superset-frontend/src/SqlLab/components/App.jsx +++ b/superset-frontend/src/SqlLab/components/App.jsx @@ -169,5 +169,4 @@ function mapDispatchToProps(dispatch) { }; } -export { App }; export default connect(mapStateToProps, mapDispatchToProps)(App); diff --git a/superset-frontend/src/SqlLab/components/ExploreCtasResultsButton.jsx b/superset-frontend/src/SqlLab/components/ExploreCtasResultsButton.jsx index 5c6f78f2fe5a6..0fac3d1159b8b 100644 --- a/superset-frontend/src/SqlLab/components/ExploreCtasResultsButton.jsx +++ b/superset-frontend/src/SqlLab/components/ExploreCtasResultsButton.jsx @@ -128,7 +128,6 @@ function mapDispatchToProps(dispatch) { }; } -export { ExploreCtasResultsButton }; export default connect( mapStateToProps, mapDispatchToProps, diff --git a/superset-frontend/src/SqlLab/components/ExploreResultsButton.jsx b/superset-frontend/src/SqlLab/components/ExploreResultsButton.jsx index 0e3661d9a59a5..388e74f195fad 100644 --- a/superset-frontend/src/SqlLab/components/ExploreResultsButton.jsx +++ b/superset-frontend/src/SqlLab/components/ExploreResultsButton.jsx @@ -253,7 +253,6 @@ function mapDispatchToProps(dispatch) { }; } -export { ExploreResultsButton }; export default connect( mapStateToProps, mapDispatchToProps, diff --git a/superset-frontend/src/SqlLab/components/HighlightedSql.jsx b/superset-frontend/src/SqlLab/components/HighlightedSql.jsx index 57ce579106ea3..b143b37c74f76 100644 --- a/superset-frontend/src/SqlLab/components/HighlightedSql.jsx +++ b/superset-frontend/src/SqlLab/components/HighlightedSql.jsx @@ -18,16 +18,14 @@ */ import React from 'react'; import PropTypes from 'prop-types'; -import SyntaxHighlighter, { - registerLanguage, -} from 'react-syntax-highlighter/dist/light'; -import sql from 'react-syntax-highlighter/dist/languages/hljs/sql'; -import github from 'react-syntax-highlighter/dist/styles/hljs/github'; +import SyntaxHighlighter from 'react-syntax-highlighter/dist/cjs/light'; +import sql from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql'; +import github from 'react-syntax-highlighter/dist/cjs/styles/hljs/github'; import { t } from '@superset-ui/core'; import ModalTrigger from '../../components/ModalTrigger'; -registerLanguage('sql', sql); +SyntaxHighlighter.registerLanguage('sql', sql); const defaultProps = { maxWidth: 50, diff --git a/superset-frontend/src/SqlLab/components/LimitControl.tsx b/superset-frontend/src/SqlLab/components/LimitControl.tsx index 2b924a7ae3ece..b5cca6375648a 100644 --- a/superset-frontend/src/SqlLab/components/LimitControl.tsx +++ b/superset-frontend/src/SqlLab/components/LimitControl.tsx @@ -17,7 +17,13 @@ * under the License. */ import React from 'react'; -import { FormGroup, FormControl, Overlay, Popover } from 'react-bootstrap'; +import { + FormGroup, + FormControl, + Overlay, + Popover, + FormControlProps, +} from 'react-bootstrap'; import Button from 'src/components/Button'; import { t, styled } from '@superset-ui/core'; @@ -77,7 +83,7 @@ export default class LimitControl extends React.PureComponent< } handleToggle() { - this.setState({ showOverlay: !this.state.showOverlay }); + this.setState(prevState => ({ showOverlay: !prevState.showOverlay })); } handleHide() { @@ -105,9 +111,12 @@ export default class LimitControl extends React.PureComponent< value={textValue} placeholder={t(`Max: ${this.props.maxRow}`)} bsSize="small" - // @ts-ignore - onChange={(event: React.ChangeEvent) => - this.setState({ textValue: event.target.value }) + onChange={( + event: React.FormEvent, + ) => + this.setState({ + textValue: (event.currentTarget?.value as string) ?? '', + }) } /> diff --git a/superset-frontend/src/SqlLab/components/ResultSet.tsx b/superset-frontend/src/SqlLab/components/ResultSet.tsx index 1846ddc1ae950..14b38bb0ee891 100644 --- a/superset-frontend/src/SqlLab/components/ResultSet.tsx +++ b/superset-frontend/src/SqlLab/components/ResultSet.tsx @@ -129,9 +129,9 @@ export default class ResultSet extends React.PureComponent< } toggleExploreResultsButton() { - this.setState({ - showExploreResultsButton: !this.state.showExploreResultsButton, - }); + this.setState(prevState => ({ + showExploreResultsButton: !prevState.showExploreResultsButton, + })); } changeSearch(event: React.ChangeEvent) { diff --git a/superset-frontend/src/SqlLab/components/SaveQuery.jsx b/superset-frontend/src/SqlLab/components/SaveQuery.jsx index cbda842dc0db3..9dfb7fd20c9bc 100644 --- a/superset-frontend/src/SqlLab/components/SaveQuery.jsx +++ b/superset-frontend/src/SqlLab/components/SaveQuery.jsx @@ -91,7 +91,7 @@ class SaveQuery extends React.PureComponent { } toggleSave() { - this.setState({ showSave: !this.state.showSave }); + this.setState(prevState => ({ showSave: !prevState.showSave })); } renderModalBody() { diff --git a/superset-frontend/src/SqlLab/components/ScheduleQueryButton.jsx b/superset-frontend/src/SqlLab/components/ScheduleQueryButton.jsx index f9a778dd5d59c..07312e7804b4d 100644 --- a/superset-frontend/src/SqlLab/components/ScheduleQueryButton.jsx +++ b/superset-frontend/src/SqlLab/components/ScheduleQueryButton.jsx @@ -133,7 +133,7 @@ class ScheduleQueryButton extends React.PureComponent { } toggleSchedule() { - this.setState({ showSchedule: !this.state.showSchedule }); + this.setState(prevState => ({ showSchedule: !prevState.showSchedule })); } renderModalBody() { diff --git a/superset-frontend/src/SqlLab/components/ShowSQL.tsx b/superset-frontend/src/SqlLab/components/ShowSQL.tsx index 002d16422fc7e..91134581a51d4 100644 --- a/superset-frontend/src/SqlLab/components/ShowSQL.tsx +++ b/superset-frontend/src/SqlLab/components/ShowSQL.tsx @@ -17,19 +17,14 @@ * under the License. */ import React from 'react'; -import SyntaxHighlighter, { - registerLanguage, - // @ts-ignore -} from 'react-syntax-highlighter/dist/light'; -// @ts-ignore -import sql from 'react-syntax-highlighter/dist/languages/hljs/sql'; -// @ts-ignore -import github from 'react-syntax-highlighter/dist/styles/hljs/github'; +import SyntaxHighlighter from 'react-syntax-highlighter/dist/cjs/light'; +import sql from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql'; +import github from 'react-syntax-highlighter/dist/cjs/styles/hljs/github'; import Link from '../../components/Link'; import ModalTrigger from '../../components/ModalTrigger'; -registerLanguage('sql', sql); +SyntaxHighlighter.registerLanguage('sql', sql); interface ShowSQLProps { sql: string; diff --git a/superset-frontend/src/SqlLab/components/SqlEditor.jsx b/superset-frontend/src/SqlLab/components/SqlEditor.jsx index a95dbd038bf7f..aeaa9d0bd26dd 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditor.jsx +++ b/superset-frontend/src/SqlLab/components/SqlEditor.jsx @@ -40,7 +40,7 @@ import Hotkeys from 'src/components/Hotkeys'; import LimitControl from './LimitControl'; import TemplateParamsEditor from './TemplateParamsEditor'; -import SouthPane from './SouthPane'; +import ConnectedSouthPane from './SouthPane'; import SaveQuery from './SaveQuery'; import ScheduleQueryButton from './ScheduleQueryButton'; import EstimateQueryCostButton from './EstimateQueryCostButton'; @@ -260,7 +260,9 @@ class SqlEditor extends React.PureComponent { } handleToggleAutocompleteEnabled = () => { - this.setState({ autocompleteEnabled: !this.state.autocompleteEnabled }); + this.setState(prevState => ({ + autocompleteEnabled: !prevState.autocompleteEnabled, + })); }; handleWindowResize() { @@ -389,7 +391,7 @@ class SqlEditor extends React.PureComponent { /> {this.renderEditorBottomBar(hotkeys)} - query.sqlEditorId === nextActiveQeId, + ); if (!areArraysShallowEqual(queriesArray, this.state.queriesArray)) { this.setState({ queriesArray }); } @@ -281,7 +278,7 @@ class TabbedSqlEditors extends React.PureComponent { } toggleLeftBar() { - this.setState({ hideLeftBar: !this.state.hideLeftBar }); + this.setState(prevState => ({ hideLeftBar: !prevState.hideLeftBar })); } render() { @@ -315,6 +312,7 @@ class TabbedSqlEditors extends React.PureComponent { <> {isSelected && ( this.removeQueryEditor(qe)} + data-test="close-tab-menu-option" >
@@ -399,12 +398,13 @@ class TabbedSqlEditors extends React.PureComponent { onSelect={this.handleSelect.bind(this)} id="a11y-query-editor-tabs" className="SqlEditorTabs" + data-test="sql-editor-tabs" > {editors} - +  
} @@ -442,6 +442,4 @@ function mapDispatchToProps(dispatch) { }; } -export { TabbedSqlEditors }; - export default connect(mapStateToProps, mapDispatchToProps)(TabbedSqlEditors); diff --git a/superset-frontend/src/SqlLab/components/TableElement.jsx b/superset-frontend/src/SqlLab/components/TableElement.jsx index c65afd476e5c1..aeca231e7a08d 100644 --- a/superset-frontend/src/SqlLab/components/TableElement.jsx +++ b/superset-frontend/src/SqlLab/components/TableElement.jsx @@ -85,7 +85,7 @@ class TableElement extends React.PureComponent { } toggleSortColumns() { - this.setState({ sortColumns: !this.state.sortColumns }); + this.setState(prevState => ({ sortColumns: !prevState.sortColumns })); } removeFromStore() { @@ -110,10 +110,9 @@ class TableElement extends React.PureComponent { /> ); } - let latest = []; - for (const k in table.partitions.latest) { - latest.push(`${k}=${table.partitions.latest[k]}`); - } + let latest = Object.entries(table.partitions.latest).map( + ([key, value]) => `${key}=${value}`, + ); latest = latest.join('/'); header = ( diff --git a/superset-frontend/src/SqlLab/reducers/sqlLab.js b/superset-frontend/src/SqlLab/reducers/sqlLab.js index a64a8b0e4694e..97f3267840df4 100644 --- a/superset-frontend/src/SqlLab/reducers/sqlLab.js +++ b/superset-frontend/src/SqlLab/reducers/sqlLab.js @@ -493,8 +493,7 @@ export default function sqlLabReducer(state = {}, action) { // Fetch the updates to the queries present in the store. let change = false; let { queriesLastUpdate } = state; - for (const id in action.alteredQueries) { - const changedQuery = action.alteredQueries[id]; + Object.entries(action.alteredQueries).forEach(([id, changedQuery]) => { if ( !state.queries.hasOwnProperty(id) || (state.queries[id].state !== 'stopped' && @@ -506,7 +505,7 @@ export default function sqlLabReducer(state = {}, action) { newQueries[id] = { ...state.queries[id], ...changedQuery }; change = true; } - } + }); if (!change) { newQueries = state.queries; } diff --git a/superset-frontend/src/components/AlteredSliceTag.jsx b/superset-frontend/src/components/AlteredSliceTag.jsx index ace9694fe85e1..34244e2827c32 100644 --- a/superset-frontend/src/components/AlteredSliceTag.jsx +++ b/superset-frontend/src/components/AlteredSliceTag.jsx @@ -76,19 +76,17 @@ export default class AlteredSliceTag extends React.Component { const fdKeys = Object.keys(cfd); const diffs = {}; - for (const fdKey of fdKeys) { - // Ignore values that are undefined/nonexisting in either + fdKeys.forEach(fdKey => { if (!ofd[fdKey] && !cfd[fdKey]) { - continue; + return; } - // Ignore obsolete legacy filters if (['filters', 'having', 'having_filters', 'where'].includes(fdKey)) { - continue; + return; } if (!this.isEqualish(ofd[fdKey], cfd[fdKey])) { diffs[fdKey] = { before: ofd[fdKey], after: cfd[fdKey] }; } - } + }); return diffs; } @@ -149,7 +147,7 @@ export default class AlteredSliceTag extends React.Component { renderRows() { const { diffs } = this.state; const rows = []; - for (const key in diffs) { + Object.entries(diffs).forEach(([key, diff]) => { rows.push( - {this.formatValue(diffs[key].before, key)} - {this.formatValue(diffs[key].after, key)} + {this.formatValue(diff.before, key)} + {this.formatValue(diff.after, key)} , ); - } + }); return rows; } diff --git a/superset-frontend/src/components/Checkbox/index.tsx b/superset-frontend/src/components/Checkbox/index.tsx index 621ce1755c56d..6bff6d8947f4e 100644 --- a/superset-frontend/src/components/Checkbox/index.tsx +++ b/superset-frontend/src/components/Checkbox/index.tsx @@ -30,8 +30,14 @@ interface CheckboxProps { } const Styles = styled.span` - &, - & svg { + cursor: pointer; + &.primary { + color: ${({ theme }) => theme.colors.primary.base}; + } + &.grayscale { + color: ${({ theme }) => theme.colors.grayscale.light1}; + } + svg { vertical-align: top; } `; @@ -39,6 +45,7 @@ const Styles = styled.span` export default function Checkbox({ checked, onChange, style }: CheckboxProps) { return ( { onChange(!checked); diff --git a/superset-frontend/src/components/CheckboxIcons.tsx b/superset-frontend/src/components/CheckboxIcons.tsx index 2c94c863fb04f..a9addf8159e5c 100644 --- a/superset-frontend/src/components/CheckboxIcons.tsx +++ b/superset-frontend/src/components/CheckboxIcons.tsx @@ -28,7 +28,7 @@ export const CheckboxChecked = () => ( > @@ -44,7 +44,7 @@ export const CheckboxHalfChecked = () => ( > @@ -60,7 +60,7 @@ export const CheckboxUnchecked = () => ( > diff --git a/superset-frontend/src/components/DeleteModal.tsx b/superset-frontend/src/components/DeleteModal.tsx index 62421ea128fb1..6e2bc90901b07 100644 --- a/superset-frontend/src/components/DeleteModal.tsx +++ b/superset-frontend/src/components/DeleteModal.tsx @@ -18,7 +18,7 @@ */ import { t, styled } from '@superset-ui/core'; import React, { useState } from 'react'; -import { FormGroup, FormControl } from 'react-bootstrap'; +import { FormGroup, FormControl, FormControlProps } from 'react-bootstrap'; import Modal from 'src/components/Modal'; import FormLabel from 'src/components/FormLabel'; @@ -70,10 +70,12 @@ export default function DeleteModal({ id="delete" type="text" bsSize="sm" - // @ts-ignore - onChange={(event: React.ChangeEvent) => - setDisableChange(event.target.value.toUpperCase() !== 'DELETE') - } + onChange={( + event: React.FormEvent, + ) => { + const targetValue = (event.currentTarget?.value as string) ?? ''; + setDisableChange(targetValue.toUpperCase() !== 'DELETE'); + }} /> diff --git a/superset-frontend/src/components/FilterableTable/FilterableTable.tsx b/superset-frontend/src/components/FilterableTable/FilterableTable.tsx index 2df936f813334..77b27108e5106 100644 --- a/superset-frontend/src/components/FilterableTable/FilterableTable.tsx +++ b/superset-frontend/src/components/FilterableTable/FilterableTable.tsx @@ -17,7 +17,6 @@ * under the License. */ import { List } from 'immutable'; -// @ts-ignore import JSONbig from 'json-bigint'; import React, { PureComponent } from 'react'; import JSONTree from 'react-json-tree'; @@ -26,11 +25,11 @@ import { Grid, ScrollSync, SortDirection, + SortDirectionType, SortIndicator, Table, - SortDirectionType, } from 'react-virtualized'; -import { t, getMultipleTextDimensions } from '@superset-ui/core'; +import { getMultipleTextDimensions, t } from '@superset-ui/core'; import Button from '../Button'; import CopyToClipboard from '../CopyToClipboard'; @@ -185,15 +184,12 @@ export default class FilterableTable extends PureComponent< const PADDING = 40; // accounts for cell padding and width of sorting icon const widthsByColumnKey = {}; const cellContent = [].concat( - ...this.props.orderedColumnKeys.map(key => - this.list - .map((data: Datum) => - this.getCellContent({ cellData: data[key], columnKey: key }), - ) - // @ts-ignore - .push(key) - .toJS(), - ), + ...this.props.orderedColumnKeys.map(key => { + const cellContentList = this.list.map((data: Datum) => + this.getCellContent({ cellData: data[key], columnKey: key }), + ) as List; + return cellContentList.push(key).toJS(); + }), ); const colWidths = getMultipleTextDimensions({ @@ -223,7 +219,7 @@ export default class FilterableTable extends PureComponent< }: { cellData: CellDataType; columnKey: string; - }) { + }): string | JSX.Element { if (cellData === null) { return NULL; } @@ -241,24 +237,22 @@ export default class FilterableTable extends PureComponent< } formatTableData(data: Record[]): Datum[] { - const formattedData = data.map(row => { + return data.map(row => { const newRow = {}; - for (const k in row) { - const val = row[k]; + Object.entries(row).forEach(([key, val]) => { if (['string', 'number'].indexOf(typeof val) >= 0) { - newRow[k] = val; + newRow[key] = val; } else { - newRow[k] = val === null ? null : JSONbig.stringify(val); + newRow[key] = val === null ? null : JSONbig.stringify(val); } - } + }); return newRow; }); - return formattedData; } hasMatch(text: string, row: Datum) { - const values = []; - for (const key in row) { + const values: string[] = []; + Object.keys(row).forEach(key => { if (row.hasOwnProperty(key)) { const cellValue = row[key]; if (typeof cellValue === 'string') { @@ -270,7 +264,7 @@ export default class FilterableTable extends PureComponent< values.push(cellValue.toString()); } } - } + }); const lowerCaseText = text.toLowerCase(); return values.some(v => v.includes(lowerCaseText)); } diff --git a/superset-frontend/src/components/Link.tsx b/superset-frontend/src/components/Link.tsx index f35bdf7251ce9..6b75ff96b7b1f 100644 --- a/superset-frontend/src/components/Link.tsx +++ b/superset-frontend/src/components/Link.tsx @@ -17,7 +17,6 @@ * under the License. */ import React, { ReactNode } from 'react'; -// @ts-ignore import { OverlayTrigger, Tooltip } from 'react-bootstrap'; interface Props { diff --git a/superset-frontend/src/components/OmniContainer.jsx b/superset-frontend/src/components/OmniContainer.jsx index 3020d9a82a596..7249f3f105c29 100644 --- a/superset-frontend/src/components/OmniContainer.jsx +++ b/superset-frontend/src/components/OmniContainer.jsx @@ -70,7 +70,7 @@ class OmniContainer extends React.Component { show_omni: !this.state.showOmni, }); - this.setState({ showOmni: !this.state.showOmni }); + this.setState(prevState => ({ showOmni: !prevState.showOmni })); document.getElementsByClassName('Omnibar')[0].focus(); } diff --git a/superset-frontend/src/components/Select/SupersetStyledSelect.tsx b/superset-frontend/src/components/Select/SupersetStyledSelect.tsx index 2728f4379943f..e605e77fb7289 100644 --- a/superset-frontend/src/components/Select/SupersetStyledSelect.tsx +++ b/superset-frontend/src/components/Select/SupersetStyledSelect.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import React, { SyntheticEvent, MutableRefObject } from 'react'; +import React, { SyntheticEvent, MutableRefObject, ComponentType } from 'react'; import { merge } from 'lodash'; import BasicSelect, { OptionTypeBase, @@ -39,6 +39,7 @@ import { SortableContainerProps, } from 'react-sortable-hoc'; import arrayMove from 'array-move'; +import { Props as SelectProps } from 'react-select/src/Select'; import { WindowedSelectComponentType, WindowedSelectProps, @@ -93,9 +94,11 @@ export type SupersetStyledSelectProps< function styled< OptionType extends OptionTypeBase, - SelectComponentType extends WindowedSelectComponentType< + SelectComponentType extends + | WindowedSelectComponentType + | ComponentType> = WindowedSelectComponentType< OptionType - > = WindowedSelectComponentType + > >(SelectComponent: SelectComponentType) { type SelectProps = SupersetStyledSelectProps; type Components = SelectComponents; @@ -221,8 +224,11 @@ function styled< // Handle onPaste event if (onPaste) { const Input = components.Input || defaultComponents.Input; - // @ts-ignore (needed for passing `onPaste`) - components.Input = props => ; + components.Input = props => ( +
+ +
+ ); } // for CreaTable if (SelectComponent === WindowedCreatableSelect) { @@ -287,7 +293,5 @@ export const Select = styled(WindowedSelect); export const AsyncSelect = styled(WindowedAsyncSelect); export const CreatableSelect = styled(WindowedCreatableSelect); export const AsyncCreatableSelect = styled(WindowedAsyncCreatableSelect); -// Wrap with async pagination (infinite scroll). Cannot use windowed since options are appended dynamically which causes focus jumping -// @ts-ignore export const PaginatedSelect = withAsyncPaginate(styled(BasicSelect)); export default Select; diff --git a/superset-frontend/src/components/Select/WindowedSelect/WindowedMenuList.tsx b/superset-frontend/src/components/Select/WindowedSelect/WindowedMenuList.tsx index ef7d94a673bac..f3d4ca1fcf046 100644 --- a/superset-frontend/src/components/Select/WindowedSelect/WindowedMenuList.tsx +++ b/superset-frontend/src/components/Select/WindowedSelect/WindowedMenuList.tsx @@ -22,6 +22,7 @@ import React, { Component, FunctionComponent, RefObject, + ReactElement, } from 'react'; import { ListChildComponentProps, @@ -53,10 +54,15 @@ export type WindowedMenuListProps = { * If may also be `Component>[]` but we are not supporting * grouped options just yet. */ + +type MenuListPropsChildren = + | Component>[] + | ReactElement[]; + export type MenuListProps< OptionType extends OptionTypeBase > = MenuListComponentProps & { - children: Component>[]; + children: MenuListPropsChildren; // theme is not present with built-in @types/react-select, but is actually // available via CommonProps. theme?: ThemeConfig; @@ -68,7 +74,7 @@ const DEFAULT_OPTION_HEIGHT = 30; /** * Get the index of the last selected option. */ -function getLastSelected(children: Component[]) { +function getLastSelected(children: MenuListPropsChildren) { return Array.isArray(children) ? children.findIndex( ({ props: { isFocused = false } = {} }) => isFocused, @@ -132,7 +138,6 @@ export default function WindowedMenuList({ return ( ({ }) { const { windowThreshold = DEFAULT_WINDOW_THRESHOLD } = props.selectProps; if (Array.isArray(children) && children.length > windowThreshold) { - // @ts-ignore - return {children}; + return ( + + {children as ReactElement[]} + + ); } return {children}; } diff --git a/superset-frontend/src/components/Select/styles.tsx b/superset-frontend/src/components/Select/styles.tsx index 7158578bb4d84..e5f844ab142e6 100644 --- a/superset-frontend/src/components/Select/styles.tsx +++ b/superset-frontend/src/components/Select/styles.tsx @@ -17,7 +17,7 @@ * under the License. */ import React, { CSSProperties } from 'react'; -import { css, SerializedStyles } from '@emotion/core'; +import { css, SerializedStyles, ClassNames } from '@emotion/core'; import { supersetTheme } from '@superset-ui/core'; import { Styles, @@ -245,18 +245,18 @@ const { ClearIndicator, DropdownIndicator, Option } = defaultComponents; export const DEFAULT_COMPONENTS: SelectComponentsConfig = { Option: ({ children, innerProps, data, ...props }) => ( - + + {({ css }) => ( + + )} + ), ClearIndicator: props => ( diff --git a/superset-frontend/src/dashboard/components/SaveModal.jsx b/superset-frontend/src/dashboard/components/SaveModal.jsx index eb2b0bd9b8676..3e69d538317e8 100644 --- a/superset-frontend/src/dashboard/components/SaveModal.jsx +++ b/superset-frontend/src/dashboard/components/SaveModal.jsx @@ -75,7 +75,9 @@ class SaveModal extends React.PureComponent { } toggleDuplicateSlices() { - this.setState({ duplicateSlices: !this.state.duplicateSlices }); + this.setState(prevState => ({ + duplicateSlices: !prevState.duplicateSlices, + })); } handleSaveTypeChange(event) { diff --git a/superset-frontend/src/dashboard/components/SliceAdder.jsx b/superset-frontend/src/dashboard/components/SliceAdder.jsx index 5bfc1441602c1..9034a8105a380 100644 --- a/superset-frontend/src/dashboard/components/SliceAdder.jsx +++ b/superset-frontend/src/dashboard/components/SliceAdder.jsx @@ -135,23 +135,23 @@ class SliceAdder extends React.Component { } searchUpdated(searchTerm) { - this.setState({ + this.setState(prevState => ({ searchTerm, filteredSlices: this.getFilteredSortedSlices( searchTerm, - this.state.sortBy, + prevState.sortBy, ), - }); + })); } handleSelect(sortBy) { - this.setState({ + this.setState(prevState => ({ sortBy, filteredSlices: this.getFilteredSortedSlices( - this.state.searchTerm, + prevState.searchTerm, sortBy, ), - }); + })); } rowRenderer({ key, index, style }) { diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls.jsx b/superset-frontend/src/dashboard/components/SliceHeaderControls.jsx index 0e6b188685ca5..728954099a930 100644 --- a/superset-frontend/src/dashboard/components/SliceHeaderControls.jsx +++ b/superset-frontend/src/dashboard/components/SliceHeaderControls.jsx @@ -103,9 +103,9 @@ class SliceHeaderControls extends React.PureComponent { } toggleControls() { - this.setState({ - showControls: !this.state.showControls, - }); + this.setState(prevState => ({ + showControls: !prevState.showControls, + })); } handleToggleFullSize() { diff --git a/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx b/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx index 09a1ae54ac636..10a61cf0ca93d 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx @@ -163,7 +163,7 @@ class ChartHolder extends React.Component { } handleToggleFullSize() { - this.setState(() => ({ isFullSize: !this.state.isFullSize })); + this.setState(prevState => ({ isFullSize: !prevState.isFullSize })); } render() { diff --git a/superset-frontend/src/datasource/ChangeDatasourceModal.tsx b/superset-frontend/src/datasource/ChangeDatasourceModal.tsx index d2c462c5712a2..b5895c122beb9 100644 --- a/superset-frontend/src/datasource/ChangeDatasourceModal.tsx +++ b/superset-frontend/src/datasource/ChangeDatasourceModal.tsx @@ -19,7 +19,7 @@ import React, { FunctionComponent, useState, useRef } from 'react'; // @ts-ignore import { Table } from 'reactable-arc'; -import { Alert, FormControl, Modal } from 'react-bootstrap'; +import { Alert, FormControl, FormControlProps, Modal } from 'react-bootstrap'; import { SupersetClient, t } from '@superset-ui/core'; import getClientErrorObject from '../utils/getClientErrorObject'; @@ -114,8 +114,10 @@ const ChangeDatasourceModal: FunctionComponent = ({ searchRef = ref; }; - const changeSearch = (event: React.ChangeEvent) => { - setFilter(event.target.value); + const changeSearch = ( + event: React.FormEvent, + ) => { + setFilter((event.currentTarget?.value as string) ?? ''); }; return ( @@ -136,7 +138,6 @@ const ChangeDatasourceModal: FunctionComponent = ({ bsSize="sm" value={filter} placeholder={t('Search / Filter')} - // @ts-ignore onChange={changeSearch} /> diff --git a/superset-frontend/src/datasource/DatasourceEditor.jsx b/superset-frontend/src/datasource/DatasourceEditor.jsx index 5efc4328097b0..93db71ce6576b 100644 --- a/superset-frontend/src/datasource/DatasourceEditor.jsx +++ b/superset-frontend/src/datasource/DatasourceEditor.jsx @@ -246,7 +246,7 @@ const defaultProps = { onChange: () => {}, }; -export class DatasourceEditor extends React.PureComponent { +class DatasourceEditor extends React.PureComponent { constructor(props) { super(props); this.state = { @@ -289,7 +289,10 @@ export class DatasourceEditor extends React.PureComponent { onDatasourcePropChange(attr, value) { const datasource = { ...this.state.datasource, [attr]: value }; - this.setState({ datasource }, this.onDatasourceChange(datasource)); + this.setState( + prevState => ({ datasource: { ...prevState.datasource, [attr]: value } }), + this.onDatasourceChange(datasource), + ); } setColumns(obj) { diff --git a/superset-frontend/src/datasource/DatasourceModal.tsx b/superset-frontend/src/datasource/DatasourceModal.tsx index 9ff1de7f6b12d..2a7227642c284 100644 --- a/superset-frontend/src/datasource/DatasourceModal.tsx +++ b/superset-frontend/src/datasource/DatasourceModal.tsx @@ -19,7 +19,6 @@ import React, { FunctionComponent, useState, useRef } from 'react'; import { Alert, Modal } from 'react-bootstrap'; import Button from 'src/components/Button'; -// @ts-ignore import Dialog from 'react-bootstrap-dialog'; import { t, SupersetClient } from '@superset-ui/core'; import AsyncEsmComponent from 'src/components/AsyncEsmComponent'; diff --git a/superset-frontend/src/explore/components/AdhocMetricEditPopover.jsx b/superset-frontend/src/explore/components/AdhocMetricEditPopover.jsx index 1e49bc6a643cc..3e0c465b7f6ea 100644 --- a/superset-frontend/src/explore/components/AdhocMetricEditPopover.jsx +++ b/superset-frontend/src/explore/components/AdhocMetricEditPopover.jsx @@ -88,40 +88,41 @@ export default class AdhocMetricEditPopover extends React.Component { } onColumnChange(column) { - this.setState({ - adhocMetric: this.state.adhocMetric.duplicateWith({ + this.setState(prevState => ({ + adhocMetric: prevState.adhocMetric.duplicateWith({ column, expressionType: EXPRESSION_TYPES.SIMPLE, }), - }); + })); } onAggregateChange(aggregate) { // we construct this object explicitly to overwrite the value in the case aggregate is null - this.setState({ - adhocMetric: this.state.adhocMetric.duplicateWith({ + this.setState(prevState => ({ + adhocMetric: prevState.adhocMetric.duplicateWith({ aggregate, expressionType: EXPRESSION_TYPES.SIMPLE, }), - }); + })); } onSqlExpressionChange(sqlExpression) { - this.setState({ - adhocMetric: this.state.adhocMetric.duplicateWith({ + this.setState(prevState => ({ + adhocMetric: prevState.adhocMetric.duplicateWith({ sqlExpression, expressionType: EXPRESSION_TYPES.SQL, }), - }); + })); } onLabelChange(e) { - this.setState({ - adhocMetric: this.state.adhocMetric.duplicateWith({ - label: e.target.value, + const label = e.target.value; + this.setState(prevState => ({ + adhocMetric: prevState.adhocMetric.duplicateWith({ + label, hasCustomLabel: true, }), - }); + })); } onDragDown(e) { diff --git a/superset-frontend/src/explore/components/ControlPanelSection.jsx b/superset-frontend/src/explore/components/ControlPanelSection.jsx index 9913b78034410..71cd56ae2932a 100644 --- a/superset-frontend/src/explore/components/ControlPanelSection.jsx +++ b/superset-frontend/src/explore/components/ControlPanelSection.jsx @@ -44,7 +44,7 @@ export default class ControlPanelSection extends React.Component { } toggleExpand() { - this.setState({ expanded: !this.state.expanded }); + this.setState(prevState => ({ expanded: !prevState.expanded })); } renderHeader() { diff --git a/superset-frontend/src/explore/components/DisplayQueryButton.jsx b/superset-frontend/src/explore/components/DisplayQueryButton.jsx index cc0ba2f87787b..7d07651618ac2 100644 --- a/superset-frontend/src/explore/components/DisplayQueryButton.jsx +++ b/superset-frontend/src/explore/components/DisplayQueryButton.jsx @@ -20,14 +20,12 @@ import React from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import PropTypes from 'prop-types'; -import SyntaxHighlighter, { - registerLanguage, -} from 'react-syntax-highlighter/light'; -import htmlSyntax from 'react-syntax-highlighter/languages/hljs/htmlbars'; -import markdownSyntax from 'react-syntax-highlighter/languages/hljs/markdown'; -import sqlSyntax from 'react-syntax-highlighter/languages/hljs/sql'; -import jsonSyntax from 'react-syntax-highlighter/languages/hljs/json'; -import github from 'react-syntax-highlighter/styles/hljs/github'; +import SyntaxHighlighter from 'react-syntax-highlighter/dist/cjs/light'; +import htmlSyntax from 'react-syntax-highlighter/dist/cjs/languages/hljs/htmlbars'; +import markdownSyntax from 'react-syntax-highlighter/dist/cjs/languages/hljs/markdown'; +import sqlSyntax from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql'; +import jsonSyntax from 'react-syntax-highlighter/dist/cjs/languages/hljs/json'; +import github from 'react-syntax-highlighter/dist/cjs/styles/hljs/github'; import { DropdownButton, MenuItem, @@ -53,10 +51,10 @@ import { import PropertiesModal from './PropertiesModal'; import { sliceUpdated } from '../actions/exploreActions'; -registerLanguage('markdown', markdownSyntax); -registerLanguage('html', htmlSyntax); -registerLanguage('sql', sqlSyntax); -registerLanguage('json', jsonSyntax); +SyntaxHighlighter.registerLanguage('markdown', markdownSyntax); +SyntaxHighlighter.registerLanguage('html', htmlSyntax); +SyntaxHighlighter.registerLanguage('sql', sqlSyntax); +SyntaxHighlighter.registerLanguage('json', jsonSyntax); const propTypes = { onOpenInEditor: PropTypes.func, diff --git a/superset-frontend/src/explore/components/ExploreActionButtons.jsx b/superset-frontend/src/explore/components/ExploreActionButtons.jsx index b509de9832c83..26ba334387891 100644 --- a/superset-frontend/src/explore/components/ExploreActionButtons.jsx +++ b/superset-frontend/src/explore/components/ExploreActionButtons.jsx @@ -23,7 +23,7 @@ import { t } from '@superset-ui/core'; import URLShortLinkButton from '../../components/URLShortLinkButton'; import EmbedCodeButton from './EmbedCodeButton'; -import DisplayQueryButton from './DisplayQueryButton'; +import ConnectedDisplayQueryButton from './DisplayQueryButton'; import { exportChart, getExploreLongUrl } from '../exploreUtils'; const propTypes = { @@ -100,7 +100,7 @@ export default function ExploreActionButtons({ .csv )} - ({ showModal: !prevState.showModal })); } hasErrors() { @@ -293,20 +293,18 @@ class ExploreViewContainer extends React.Component { renderErrorMessage() { // Returns an error message as a node if any errors are in the store - const errors = []; - const ctrls = this.props.controls; - for (const controlName in this.props.controls) { - const control = this.props.controls[controlName]; - if (control.validationErrors && control.validationErrors.length > 0) { - errors.push( -
- {t('Control labeled ')} - {` "${control.label}" `} - {control.validationErrors.join('. ')} -
, - ); - } - } + const errors = Object.entries(this.props.controls) + .filter( + ([, control]) => + control.validationErrors && control.validationErrors.length > 0, + ) + .map(([key, control]) => ( +
+ {t('Control labeled ')} + {` "${control.label}" `} + {control.validationErrors.join('. ')} +
+ )); let errorMessage; if (errors.length > 0) { errorMessage =
{errors}
; @@ -354,7 +352,7 @@ class ExploreViewContainer extends React.Component { errorMessage={this.renderErrorMessage()} datasourceType={this.props.datasource_type} /> - ) => - setName(event.target.value) - } + onChange={( + event: React.FormEvent, + ) => setName((event.currentTarget?.value as string) ?? '')} /> @@ -190,9 +188,10 @@ function PropertiesModal({ slice, onHide, onSave }: InternalProps) { componentClass="textarea" bsSize="sm" value={description} - // @ts-ignore - onChange={(event: React.ChangeEvent) => - setDescription(event.target.value) + onChange={( + event: React.FormEvent, + ) => + setDescription((event.currentTarget?.value as string) ?? '') } style={{ maxWidth: '100%' }} /> @@ -212,10 +211,13 @@ function PropertiesModal({ slice, onHide, onSave }: InternalProps) { type="text" bsSize="sm" value={cacheTimeout} - // @ts-ignore - onChange={(event: React.ChangeEvent) => - setCacheTimeout(event.target.value.replace(/[^0-9]/, '')) - } + onChange={( + event: React.FormEvent, + ) => { + const targetValue = + (event.currentTarget?.value as string) ?? ''; + setCacheTimeout(targetValue.replace(/[^0-9]/, '')); + }} />

{t( diff --git a/superset-frontend/src/explore/components/SaveModal.jsx b/superset-frontend/src/explore/components/SaveModal.jsx index 1c8626c6dc6f5..c3a42e654535c 100644 --- a/superset-frontend/src/explore/components/SaveModal.jsx +++ b/superset-frontend/src/explore/components/SaveModal.jsx @@ -250,5 +250,4 @@ function mapStateToProps({ explore, saveModal }) { }; } -export { SaveModal }; export default connect(mapStateToProps, () => ({}))(SaveModal); diff --git a/superset-frontend/src/explore/components/controls/AnnotationLayer.jsx b/superset-frontend/src/explore/components/controls/AnnotationLayer.jsx index 8c0b750b2f736..cded668bf7434 100644 --- a/superset-frontend/src/explore/components/controls/AnnotationLayer.jsx +++ b/superset-frontend/src/explore/components/controls/AnnotationLayer.jsx @@ -34,8 +34,9 @@ import SelectControl from './SelectControl'; import TextControl from './TextControl'; import CheckboxControl from './CheckboxControl'; -import ANNOTATION_TYPES, { +import { ANNOTATION_SOURCE_TYPES, + ANNOTATION_TYPES, ANNOTATION_TYPES_METADATA, DEFAULT_ANNOTATION_TYPE, requiresQuery, @@ -332,7 +333,7 @@ export default class AnnotationLayer extends React.PureComponent { annotation.color = annotation.color === AUTOMATIC_COLOR ? null : annotation.color; this.props.addAnnotationLayer(annotation); - this.setState({ isNew: false, oldName: this.state.name }); + this.setState(prevState => ({ isNew: false, oldName: prevState.name })); } } diff --git a/superset-frontend/src/explore/components/controls/BoundsControl.jsx b/superset-frontend/src/explore/components/controls/BoundsControl.jsx index 5c7c567f33fbb..efdae19a144b5 100644 --- a/superset-frontend/src/explore/components/controls/BoundsControl.jsx +++ b/superset-frontend/src/explore/components/controls/BoundsControl.jsx @@ -47,19 +47,21 @@ export default class BoundsControl extends React.Component { } onMinChange(event) { + const min = event.target.value; this.setState( - { - minMax: [event.target.value, this.state.minMax[1]], - }, + prevState => ({ + minMax: [min, prevState.minMax[1]], + }), this.onChange, ); } onMaxChange(event) { + const max = event.target.value; this.setState( - { - minMax: [this.state.minMax[0], event.target.value], - }, + prevState => ({ + minMax: [prevState.minMax[0], max], + }), this.onChange, ); } diff --git a/superset-frontend/src/explore/components/controls/DateFilterControl.jsx b/superset-frontend/src/explore/components/controls/DateFilterControl.jsx index 9682b7561407a..bd30125153ee2 100644 --- a/superset-frontend/src/explore/components/controls/DateFilterControl.jsx +++ b/superset-frontend/src/explore/components/controls/DateFilterControl.jsx @@ -128,22 +128,23 @@ function getStateFromSeparator(value) { function getStateFromCommonTimeFrame(value) { const units = `${value.split(' ')[1]}s`; + let sinceMoment; + + if (value === 'No filter') { + sinceMoment = ''; + } else if (units === 'years') { + sinceMoment = moment().utc().startOf(units).subtract(1, units); + } else { + sinceMoment = moment().utc().startOf('day').subtract(1, units); + } + return { tab: TABS.DEFAULTS, type: TYPES.DEFAULTS, common: value, - since: - value === 'No filter' - ? '' - : moment() - .utc() - .startOf('day') - .subtract(1, units) - .format(MOMENT_FORMAT), + since: sinceMoment === '' ? '' : sinceMoment.format(MOMENT_FORMAT), until: - value === 'No filter' - ? '' - : moment().utc().startOf('day').format(MOMENT_FORMAT), + sinceMoment === '' ? '' : sinceMoment.add(1, units).format(MOMENT_FORMAT), }; } @@ -259,14 +260,14 @@ class DateFilterControl extends React.Component { const closeCalendar = (key === 'since' && this.state.sinceViewMode === 'days') || (key === 'until' && this.state.untilViewMode === 'days'); - this.setState({ + this.setState(prevState => ({ type: TYPES.CUSTOM_START_END, [key]: typeof value === 'string' ? value : value.format(MOMENT_FORMAT), - showSinceCalendar: this.state.showSinceCalendar && !closeCalendar, - showUntilCalendar: this.state.showUntilCalendar && !closeCalendar, - sinceViewMode: closeCalendar ? 'days' : this.state.sinceViewMode, - untilViewMode: closeCalendar ? 'days' : this.state.untilViewMode, - }); + showSinceCalendar: prevState.showSinceCalendar && !closeCalendar, + showUntilCalendar: prevState.showUntilCalendar && !closeCalendar, + sinceViewMode: closeCalendar ? 'days' : prevState.sinceViewMode, + untilViewMode: closeCalendar ? 'days' : prevState.untilViewMode, + })); } setTypeCustomRange() { @@ -396,7 +397,6 @@ class DateFilterControl extends React.Component { )); const timeFrames = COMMON_TIME_FRAMES.map(timeFrame => { const nextState = getStateFromCommonTimeFrame(timeFrame); - const timeRange = buildTimeRangeString(nextState.since, nextState.until); return ( diff --git a/superset-frontend/src/explore/components/controls/FixedOrMetricControl.jsx b/superset-frontend/src/explore/components/controls/FixedOrMetricControl.jsx index 6c8edfc4d6900..d7cf050b21508 100644 --- a/superset-frontend/src/explore/components/controls/FixedOrMetricControl.jsx +++ b/superset-frontend/src/explore/components/controls/FixedOrMetricControl.jsx @@ -90,10 +90,9 @@ export default class FixedOrMetricControl extends React.Component { } toggle() { - const expanded = !this.state.expanded; - this.setState({ - expanded, - }); + this.setState(prevState => ({ + expanded: !prevState.expanded, + })); } render() { diff --git a/superset-frontend/src/explore/components/controls/SelectControl.jsx b/superset-frontend/src/explore/components/controls/SelectControl.jsx index 3182434bc8d96..7ec6c699215c5 100644 --- a/superset-frontend/src/explore/components/controls/SelectControl.jsx +++ b/superset-frontend/src/explore/components/controls/SelectControl.jsx @@ -103,7 +103,7 @@ export default class SelectControl extends React.PureComponent { if (opt) { if (this.props.multi) { optionValue = []; - for (const o of opt) { + opt.forEach(o => { // select all options if (o.meta === true) { this.props.onChange( @@ -114,7 +114,7 @@ export default class SelectControl extends React.PureComponent { return; } optionValue.push(o[this.props.valueKey] || o); - } + }); } else if (opt.meta === true) { return; } else { diff --git a/superset-frontend/src/explore/components/controls/SpatialControl.jsx b/superset-frontend/src/explore/components/controls/SpatialControl.jsx index 9553200aab5bb..073148be9560b 100644 --- a/superset-frontend/src/explore/components/controls/SpatialControl.jsx +++ b/superset-frontend/src/explore/components/controls/SpatialControl.jsx @@ -114,7 +114,7 @@ export default class SpatialControl extends React.Component { toggleCheckbox() { this.setState( - { reverseCheckbox: !this.state.reverseCheckbox }, + prevState => ({ reverseCheckbox: !prevState.reverseCheckbox }), this.onChange, ); } diff --git a/superset-frontend/src/explore/components/controls/VizTypeControl.jsx b/superset-frontend/src/explore/components/controls/VizTypeControl.jsx index ee1a4bf6188c6..3f1bd2b89b4a0 100644 --- a/superset-frontend/src/explore/components/controls/VizTypeControl.jsx +++ b/superset-frontend/src/explore/components/controls/VizTypeControl.jsx @@ -120,7 +120,7 @@ export default class VizTypeControl extends React.PureComponent { } toggleModal() { - this.setState({ showModal: !this.state.showModal }); + this.setState(prevState => ({ showModal: !prevState.showModal })); } changeSearch(event) { diff --git a/superset-frontend/src/explore/controlUtils.js b/superset-frontend/src/explore/controlUtils.js index a273fd46dda7d..c05e4defcc01d 100644 --- a/superset-frontend/src/explore/controlUtils.js +++ b/superset-frontend/src/explore/controlUtils.js @@ -51,22 +51,19 @@ export function validateControl(control, processedState) { /** * Find control item from control panel config. */ -function findControlItem(controlPanelSections, controlKey) { - for (const section of controlPanelSections) { - for (const controlArr of section.controlSetRows) { - for (const control of controlArr) { - if (controlKey === control) return control; - if ( - control !== null && - typeof control === 'object' && - control.name === controlKey - ) { - return control; - } - } - } - } - return null; +export function findControlItem(controlPanelSections, controlKey) { + return ( + controlPanelSections + .map(section => section.controlSetRows) + .flat(2) + .find( + control => + controlKey === control || + (control !== null && + typeof control === 'object' && + control.name === controlKey), + ) ?? null + ); } export const getControlConfig = memoizeOne(function getControlConfig( diff --git a/superset-frontend/src/explore/controls.jsx b/superset-frontend/src/explore/controls.jsx index 7b71015ef1580..1c77938671589 100644 --- a/superset-frontend/src/explore/controls.jsx +++ b/superset-frontend/src/explore/controls.jsx @@ -501,4 +501,3 @@ export const controls = { }), }, }; -export default controls; diff --git a/superset-frontend/src/explore/store.js b/superset-frontend/src/explore/store.js index 3ab2b07167aa6..083ed5866fee7 100644 --- a/superset-frontend/src/explore/store.js +++ b/superset-frontend/src/explore/store.js @@ -19,7 +19,7 @@ /* eslint camelcase: 0 */ import { getChartControlPanelRegistry } from '@superset-ui/core'; import { getAllControlsState, getFormDataFromControls } from './controlUtils'; -import controls from './controls'; +import { controls } from './controls'; function handleDeprecatedControls(formData) { // Reacffectation / handling of deprecated controls diff --git a/superset-frontend/src/featureFlags.ts b/superset-frontend/src/featureFlags.ts index db6434aa1d105..3b9acb1bfa7e1 100644 --- a/superset-frontend/src/featureFlags.ts +++ b/superset-frontend/src/featureFlags.ts @@ -38,6 +38,8 @@ export type FeatureFlagMap = { declare global { interface Window { featureFlags: FeatureFlagMap; + $: any; + jQuery: any; } } diff --git a/superset-frontend/src/modules/AnnotationTypes.js b/superset-frontend/src/modules/AnnotationTypes.js index 99c639c538c97..5741788a6a60c 100644 --- a/superset-frontend/src/modules/AnnotationTypes.js +++ b/superset-frontend/src/modules/AnnotationTypes.js @@ -77,5 +77,3 @@ export function applyNativeColumns(annotation) { } return annotation; } - -export default ANNOTATION_TYPES; diff --git a/superset-frontend/src/setup/setupApp.ts b/superset-frontend/src/setup/setupApp.ts index c51b42a508884..1eef937bc7d73 100644 --- a/superset-frontend/src/setup/setupApp.ts +++ b/superset-frontend/src/setup/setupApp.ts @@ -82,9 +82,7 @@ export default function setupApp() { // A set of hacks to allow apps to run within a FAB template // this allows for the server side generated menus to function - // @ts-ignore window.$ = $; - // @ts-ignore window.jQuery = $; require('bootstrap'); diff --git a/superset-frontend/src/utils/common.js b/superset-frontend/src/utils/common.js index 39a023bbc3cd2..033382294eb3e 100644 --- a/superset-frontend/src/utils/common.js +++ b/superset-frontend/src/utils/common.js @@ -82,14 +82,6 @@ export function getShortUrl(longUrl) { ); } -export function supersetURL(rootUrl, getParams = {}) { - const url = new URL(rootUrl, window.location.origin); - for (const k in getParams) { - url.searchParams.set(k, getParams[k]); - } - return url.href; -} - export function optionLabel(opt) { if (opt === null) { return NULL_STRING; diff --git a/superset-frontend/src/utils/getControlsForVizType.js b/superset-frontend/src/utils/getControlsForVizType.js index 0dac5be088726..ec6930f0706ba 100644 --- a/superset-frontend/src/utils/getControlsForVizType.js +++ b/superset-frontend/src/utils/getControlsForVizType.js @@ -19,7 +19,7 @@ import memoize from 'lodash/memoize'; import { getChartControlPanelRegistry } from '@superset-ui/core'; -import controls from '../explore/controls'; +import { controls } from '../explore/controls'; const getControlsForVizType = memoize(vizType => { const controlsMap = {}; diff --git a/superset-frontend/src/views/CRUD/welcome/Welcome.tsx b/superset-frontend/src/views/CRUD/welcome/Welcome.tsx index 68f6d10c6f32f..256fe3a9e4fa3 100644 --- a/superset-frontend/src/views/CRUD/welcome/Welcome.tsx +++ b/superset-frontend/src/views/CRUD/welcome/Welcome.tsx @@ -16,8 +16,16 @@ * specific language governing permissions and limitations * under the License. */ -import React, { useState } from 'react'; -import { Panel, Row, Col, Tabs, Tab, FormControl } from 'react-bootstrap'; +import React, { useCallback, useState } from 'react'; +import { + Panel, + Row, + Col, + Tabs, + Tab, + FormControl, + FormControlProps, +} from 'react-bootstrap'; import { t } from '@superset-ui/core'; import { useQueryParam, StringParam, QueryParamConfig } from 'use-query-params'; import { User } from 'src/types/bootstrapTypes'; @@ -61,12 +69,23 @@ export default function Welcome({ user }: WelcomeProps) { '', ); + const onFormControlChange = useCallback( + (e: React.FormEvent) => { + const { value } = e.currentTarget; + setSearchQuery((value as string) ?? ''); + }, + [], + ); + + const onTabsSelect = useCallback((e: any) => { + setActiveTab(e as string); + }, []); + return (

@@ -83,8 +102,7 @@ export default function Welcome({ user }: WelcomeProps) { style={{ marginTop: '25px' }} placeholder="Search" value={searchQuery} - // @ts-ignore React bootstrap types aren't quite right here - onChange={e => setSearchQuery(e.currentTarget.value)} + onChange={onFormControlChange} /> diff --git a/superset-frontend/src/visualizations/FilterBox/FilterBox.jsx b/superset-frontend/src/visualizations/FilterBox/FilterBox.jsx index 311b7171999c7..a5d33f93a400a 100644 --- a/superset-frontend/src/visualizations/FilterBox/FilterBox.jsx +++ b/superset-frontend/src/visualizations/FilterBox/FilterBox.jsx @@ -29,7 +29,7 @@ import FormLabel from 'src/components/FormLabel'; import DateFilterControl from 'src/explore/components/controls/DateFilterControl'; import ControlRow from 'src/explore/components/ControlRow'; import Control from 'src/explore/components/Control'; -import controls from 'src/explore/controls'; +import { controls } from 'src/explore/controls'; import { getExploreUrl } from 'src/explore/exploreUtils'; import OnPasteSelect from 'src/components/Select/OnPasteSelect'; import { getDashboardFilterKey } from 'src/dashboard/util/getDashboardFilterKey'; @@ -176,16 +176,21 @@ class FilterBox extends React.Component { vals = options; } } - const selectedValues = { - ...this.state.selectedValues, - [fltr]: vals, - }; - this.setState({ selectedValues, hasChanged: true }, () => { - if (this.props.instantFiltering) { - this.props.onChange({ [fltr]: vals }, false); - } - }); + this.setState( + prevState => ({ + selectedValues: { + ...prevState.selectedValues, + [fltr]: vals, + }, + hasChanged: true, + }), + () => { + if (this.props.instantFiltering) { + this.props.onChange({ [fltr]: vals }, false); + } + }, + ); } /** diff --git a/superset-frontend/webpack.config.js b/superset-frontend/webpack.config.js index 081b98fdaa9f8..51e7eb285eb13 100644 --- a/superset-frontend/webpack.config.js +++ b/superset-frontend/webpack.config.js @@ -78,7 +78,7 @@ const plugins = [ // } // } const entryFiles = {}; - for (const [entry, chunks] of Object.entries(entrypoints)) { + Object.entries(entrypoints).forEach(([entry, chunks]) => { entryFiles[entry] = { css: chunks .filter(x => x.endsWith('.css')) @@ -87,7 +87,8 @@ const plugins = [ .filter(x => x.endsWith('.js')) .map(x => path.join(output.publicPath, x)), }; - } + }); + return { ...seed, entrypoints: entryFiles, @@ -430,7 +431,7 @@ if (isDevMode) { // find all the symlinked plugins and use their source code for imports let hasSymlink = false; - for (const [pkg, version] of Object.entries(packageConfig.dependencies)) { + Object.entries(packageConfig.dependencies).forEach(([pkg, version]) => { const srcPath = `./node_modules/${pkg}/src`; if (/superset-ui/.test(pkg) && fs.existsSync(srcPath)) { console.log( @@ -441,7 +442,7 @@ if (isDevMode) { config.resolve.alias[`${pkg}$`] = `${pkg}/src`; hasSymlink = true; } - } + }); if (hasSymlink) { console.log(''); // pure cosmetic new line } diff --git a/superset/app.py b/superset/app.py index c3731a4a16567..1ef5b30531aaf 100644 --- a/superset/app.py +++ b/superset/app.py @@ -261,8 +261,8 @@ def init_views(self) -> None: if self.config["ENABLE_ROW_LEVEL_SECURITY"]: appbuilder.add_view( RowLevelSecurityFiltersModelView, - "Row Level Security Filters", - label=__("Row level security filters"), + "Row Level Security", + label=__("Row level security"), category="Security", category_label=__("Security"), icon="fa-lock", diff --git a/superset/common/query_context.py b/superset/common/query_context.py index 0801a7080f66a..ad67d119d0afb 100644 --- a/superset/common/query_context.py +++ b/superset/common/query_context.py @@ -124,17 +124,13 @@ def get_query_result(self, query_object: QueryObject) -> Dict[str, Any]: } @staticmethod - def df_metrics_to_num( # pylint: disable=no-self-use - df: pd.DataFrame, query_object: QueryObject - ) -> None: + def df_metrics_to_num(df: pd.DataFrame, query_object: QueryObject) -> None: """Converting metrics to numeric when pandas.read_sql cannot""" for col, dtype in df.dtypes.items(): if dtype.type == np.object_ and col in query_object.metrics: df[col] = pd.to_numeric(df[col], errors="coerce") - def get_data( - self, df: pd.DataFrame, - ) -> Union[str, List[Dict[str, Any]]]: # pylint: disable=no-self-use + def get_data(self, df: pd.DataFrame,) -> Union[str, List[Dict[str, Any]]]: if self.result_format == utils.ChartDataResultFormat.CSV: include_index = not isinstance(df.index, pd.RangeIndex) result = df.to_csv(index=include_index, **config["CSV_EXPORT"]) @@ -204,7 +200,7 @@ def cache_key(self, query_obj: QueryObject, **kwargs: Any) -> Optional[str]: ) return cache_key - def get_df_payload( # pylint: disable=too-many-locals,too-many-statements + def get_df_payload( # pylint: disable=too-many-statements self, query_obj: QueryObject, **kwargs: Any ) -> Dict[str, Any]: """Handles caching around the df payload retrieval""" @@ -228,7 +224,7 @@ def get_df_payload( # pylint: disable=too-many-locals,too-many-statements status = utils.QueryStatus.SUCCESS is_loaded = True stats_logger.incr("loaded_from_cache") - except Exception as ex: # pylint: disable=broad-except + except KeyError as ex: logger.exception(ex) logger.error( "Error reading cache: %s", utils.error_msg_from_exception(ex) diff --git a/superset/connectors/base/models.py b/superset/connectors/base/models.py index 1f6f032c9bf42..75792c130fb85 100644 --- a/superset/connectors/base/models.py +++ b/superset/connectors/base/models.py @@ -410,7 +410,7 @@ def get_fk_many_from_list( fkmany: List[Column], fkmany_class: Type[Union["BaseColumn", "BaseMetric"]], key_attr: str, - ) -> List[Column]: # pylint: disable=too-many-locals + ) -> List[Column]: """Update ORM one-to-many list from object list Used for syncing metrics and columns using the same code""" diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py index 667c9d4a3ee7b..49663d21421ab 100644 --- a/superset/connectors/sqla/models.py +++ b/superset/connectors/sqla/models.py @@ -16,7 +16,7 @@ # under the License. import json import logging -from collections import OrderedDict +from collections import defaultdict, OrderedDict from dataclasses import dataclass, field from datetime import datetime, timedelta from typing import Any, Dict, Hashable, List, NamedTuple, Optional, Tuple, Union @@ -35,6 +35,7 @@ Column, DateTime, desc, + Enum, ForeignKey, Integer, or_, @@ -92,8 +93,8 @@ class MetadataResult: class AnnotationDatasource(BaseDatasource): - """ Dummy object so we can query annotations using 'Viz' objects just like - regular datasources. + """Dummy object so we can query annotations using 'Viz' objects just like + regular datasources. """ cache_timeout = 0 @@ -798,11 +799,14 @@ def _get_sqla_row_level_filters( :returns: A list of SQL clauses to be ANDed together. :rtype: List[str] """ + filters_grouped: Dict[Union[int, str], List[str]] = defaultdict(list) try: - return [ - text("({})".format(template_processor.process_template(f.clause))) - for f in security_manager.get_rls_filters(self) - ] + for filter_ in security_manager.get_rls_filters(self): + clause = text( + f"({template_processor.process_template(filter_.clause)})" + ) + filters_grouped[filter_.group_key or filter_.id].append(clause) + return [or_(*clauses) for clauses in filters_grouped.values()] except TemplateError as ex: raise QueryObjectValidationError( _("Error in jinja expression in RLS filters: %(msg)s", msg=ex.message,) @@ -1371,9 +1375,9 @@ def import_obj( ) -> int: """Imports the datasource from the object to the database. - Metrics and columns and datasource will be overrided if exists. - This function can be used to import/export dashboards between multiple - superset instances. Audit metadata isn't copies over. + Metrics and columns and datasource will be overrided if exists. + This function can be used to import/export dashboards between multiple + superset instances. Audit metadata isn't copies over. """ def lookup_sqlatable(table_: "SqlaTable") -> "SqlaTable": @@ -1506,6 +1510,10 @@ class RowLevelSecurityFilter(Model, AuditMixinNullable): __tablename__ = "row_level_security_filters" id = Column(Integer, primary_key=True) + filter_type = Column( + Enum(*[filter_type.value for filter_type in utils.RowLevelSecurityFilterType]) + ) + group_key = Column(String(255), nullable=True) roles = relationship( security_manager.role_model, secondary=RLSFilterRoles, diff --git a/superset/connectors/sqla/views.py b/superset/connectors/sqla/views.py index a1e28e492b5c9..4483018fdf7a2 100644 --- a/superset/connectors/sqla/views.py +++ b/superset/connectors/sqla/views.py @@ -18,9 +18,9 @@ import logging import re from dataclasses import dataclass, field -from typing import Dict, List, Union +from typing import Any, cast, Dict, List, Union -from flask import flash, Markup, redirect +from flask import current_app, flash, Markup, redirect from flask_appbuilder import CompactCRUDMixin, expose from flask_appbuilder.actions import action from flask_appbuilder.fieldwidgets import Select2Widget @@ -41,6 +41,7 @@ DatasourceFilter, DeleteMixin, ListWidgetWithCheckboxes, + SupersetListWidget, SupersetModelView, validate_sqlatable, YamlExportMixin, @@ -241,30 +242,73 @@ class SqlMetricInlineView( # pylint: disable=too-many-ancestors edit_form_extra_fields = add_form_extra_fields +class RowLevelSecurityListWidget( + SupersetListWidget +): # pylint: disable=too-few-public-methods + template = "superset/models/rls/list.html" + + def __init__(self, **kwargs: Any): + kwargs["appbuilder"] = current_app.appbuilder + super().__init__(**kwargs) + + class RowLevelSecurityFiltersModelView( # pylint: disable=too-many-ancestors SupersetModelView, DeleteMixin ): datamodel = SQLAInterface(models.RowLevelSecurityFilter) + list_widget = cast(SupersetListWidget, RowLevelSecurityListWidget) + list_title = _("Row level security filter") show_title = _("Show Row level security filter") add_title = _("Add Row level security filter") edit_title = _("Edit Row level security filter") - list_columns = ["tables", "roles", "clause", "creator", "modified"] - order_columns = ["tables", "clause", "modified"] - edit_columns = ["tables", "roles", "clause"] + list_columns = [ + "filter_type", + "tables", + "roles", + "group_key", + "clause", + "creator", + "modified", + ] + order_columns = ["filter_type", "group_key", "clause", "modified"] + edit_columns = ["filter_type", "tables", "roles", "group_key", "clause"] show_columns = edit_columns - search_columns = ("tables", "roles", "clause") + search_columns = ("filter_type", "tables", "roles", "group_key", "clause") add_columns = edit_columns base_order = ("changed_on", "desc") description_columns = { + "filter_type": _( + "Regular filters add where clauses to queries if a user belongs to a " + "role referenced in the filter. Base filters apply filters to all queries " + "except the roles defined in the filter, and can be used to define what " + "users can see if no RLS filters within a filter group apply to them." + ), "tables": _("These are the tables this filter will be applied to."), - "roles": _("These are the roles this filter will be applied to."), + "roles": _( + "For regular filters, these are the roles this filter will be " + "applied to. For base filters, these are the roles that the " + "filter DOES NOT apply to, e.g. Admin if admin should see all " + "data." + ), + "group_key": _( + "Filters with the same group key will be ORed together within the group, " + "while different filter groups will be ANDed together. Undefined group " + "keys are treated as unique groups, i.e. are not grouped together. " + "For example, if a table has three filters, of which two are for " + "departments Finance and Marketing (group key = 'department'), and one " + "refers to the region Europe (group key = 'region'), the filter clause " + "would apply the filter (department = 'Finance' OR department = " + "'Marketing') AND (region = 'Europe')." + ), "clause": _( "This is the condition that will be added to the WHERE clause. " "For example, to only return rows for a particular client, " - "you might put in: client_id = 9" + "you might define a regular filter with the clause `client_id = 9`. To " + "display no rows unless a user belongs to a RLS filter role, a base " + "filter can be created with the clause `1 = 0` (always false)." ), } label_columns = { diff --git a/superset/dao/base.py b/superset/dao/base.py index 59791ff79bc6d..c5db30167b1bb 100644 --- a/superset/dao/base.py +++ b/superset/dao/base.py @@ -39,11 +39,11 @@ class BaseDAO: """ Child classes need to state the Model class so they don't need to implement basic create, update and delete methods - """ # pylint: disable=pointless-string-statement + """ base_filter: Optional[BaseFilter] = None """ Child classes can register base filtering to be aplied to all filter methods - """ # pylint: disable=pointless-string-statement + """ @classmethod def find_by_id(cls, model_id: int) -> Model: diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py index 7afecc20d783a..d691316836b5c 100644 --- a/superset/dashboards/api.py +++ b/superset/dashboards/api.py @@ -232,9 +232,7 @@ def post(self) -> Response: @protect() @safe @statsd_metrics - def put( # pylint: disable=too-many-return-statements, arguments-differ - self, pk: int - ) -> Response: + def put(self, pk: int) -> Response: """Changes a Dashboard --- put: @@ -286,24 +284,25 @@ def put( # pylint: disable=too-many-return-statements, arguments-differ return self.response_400(message=error.messages) try: changed_model = UpdateDashboardCommand(g.user, pk, item).run() - return self.response(200, id=changed_model.id, result=item) + response = self.response(200, id=changed_model.id, result=item) except DashboardNotFoundError: - return self.response_404() + response = self.response_404() except DashboardForbiddenError: - return self.response_403() + response = self.response_403() except DashboardInvalidError as ex: return self.response_422(message=ex.normalized_messages()) except DashboardUpdateFailedError as ex: logger.error( "Error updating model %s: %s", self.__class__.__name__, str(ex) ) - return self.response_422(message=str(ex)) + response = self.response_422(message=str(ex)) + return response @expose("/", methods=["DELETE"]) @protect() @safe @statsd_metrics - def delete(self, pk: int) -> Response: # pylint: disable=arguments-differ + def delete(self, pk: int) -> Response: """Deletes a Dashboard --- delete: @@ -353,9 +352,7 @@ def delete(self, pk: int) -> Response: # pylint: disable=arguments-differ @safe @statsd_metrics @rison(get_delete_ids_schema) - def bulk_delete( - self, **kwargs: Any - ) -> Response: # pylint: disable=arguments-differ + def bulk_delete(self, **kwargs: Any) -> Response: """Delete bulk Dashboards --- delete: diff --git a/superset/datasets/api.py b/superset/datasets/api.py index 93c2b92496e65..40b8c5edec0a2 100644 --- a/superset/datasets/api.py +++ b/superset/datasets/api.py @@ -216,9 +216,7 @@ def post(self) -> Response: @protect() @safe @statsd_metrics - def put( # pylint: disable=too-many-return-statements, arguments-differ - self, pk: int - ) -> Response: + def put(self, pk: int) -> Response: """Changes a Dataset --- put: @@ -270,24 +268,25 @@ def put( # pylint: disable=too-many-return-statements, arguments-differ return self.response_400(message=error.messages) try: changed_model = UpdateDatasetCommand(g.user, pk, item).run() - return self.response(200, id=changed_model.id, result=item) + response = self.response(200, id=changed_model.id, result=item) except DatasetNotFoundError: - return self.response_404() + response = self.response_404() except DatasetForbiddenError: - return self.response_403() + response = self.response_403() except DatasetInvalidError as ex: - return self.response_422(message=ex.normalized_messages()) + response = self.response_422(message=ex.normalized_messages()) except DatasetUpdateFailedError as ex: logger.error( "Error updating model %s: %s", self.__class__.__name__, str(ex) ) - return self.response_422(message=str(ex)) + response = self.response_422(message=str(ex)) + return response @expose("/", methods=["DELETE"]) @protect() @safe @statsd_metrics - def delete(self, pk: int) -> Response: # pylint: disable=arguments-differ + def delete(self, pk: int) -> Response: """Deletes a Dataset --- delete: diff --git a/superset/db_engine_specs/base.py b/superset/db_engine_specs/base.py index f924ade58e072..4ed9ec5e4453d 100644 --- a/superset/db_engine_specs/base.py +++ b/superset/db_engine_specs/base.py @@ -902,16 +902,18 @@ def make_label_compatible(cls, label: str) -> Union[str, quoted_name]: return label_mutated @classmethod - def get_sqla_column_type(cls, type_: str) -> Optional[TypeEngine]: + def get_sqla_column_type(cls, type_: Optional[str]) -> Optional[TypeEngine]: """ Return a sqlalchemy native column type that corresponds to the column type defined in the data source (return None to use default type inferred by - SQLAlchemy). Override `_column_type_mappings` for specific needs + SQLAlchemy). Override `column_type_mappings` for specific needs (see MSSQL for example of NCHAR/NVARCHAR handling). :param type_: Column type returned by inspector :return: SqlAlchemy column type """ + if not type_: + return None for regex, sqla_type in cls.column_type_mappings: match = regex.match(type_) if match: diff --git a/superset/db_engine_specs/presto.py b/superset/db_engine_specs/presto.py index 9b2c47b307667..bb8461311f690 100644 --- a/superset/db_engine_specs/presto.py +++ b/superset/db_engine_specs/presto.py @@ -174,7 +174,9 @@ def get_view_names( return [row[0] for row in results] @classmethod - def _create_column_info(cls, name: str, data_type: str) -> Dict[str, Any]: + def _create_column_info( + cls, name: str, data_type: types.TypeEngine + ) -> Dict[str, Any]: """ Create column info object :param name: column name @@ -265,8 +267,11 @@ def _parse_structural_column( # pylint: disable=too-many-locals,too-many-branch # overall structural data type column_type = cls.get_sqla_column_type(field_info[1]) if column_type is None: - raise NotImplementedError( - _("Unknown column type: %(col)s", col=field_info[1]) + column_type = types.String() + logger.info( + "Did not recognize type %s of column %s", + field_info[1], + field_info[0], ) if field_info[1] == "array" or field_info[1] == "row": stack.append((field_info[0], field_info[1])) @@ -381,8 +386,11 @@ def get_columns( # otherwise column is a basic data type column_type = cls.get_sqla_column_type(column.Type) if column_type is None: - raise NotImplementedError( - _("Unknown column type: %(col)s", col=column_type) + column_type = types.String() + logger.info( + "Did not recognize type %s of column %s", + str(column.Type), + str(column.Column), ) column_info = cls._create_column_info(column.Column, column_type) column_info["nullable"] = getattr(column, "Null", True) diff --git a/superset/jinja_context.py b/superset/jinja_context.py index 95ee723848a91..5a359971a707e 100644 --- a/superset/jinja_context.py +++ b/superset/jinja_context.py @@ -213,17 +213,17 @@ def __init__( extra_cache_keys: Optional[List[Any]] = None, **kwargs: Any, ) -> None: - self.database = database - self.query = query - self.schema = None + self._database = database + self._query = query + self._schema = None if query and query.schema: - self.schema = query.schema + self._schema = query.schema elif table: - self.schema = table.schema + self._schema = table.schema extra_cache = ExtraCache(extra_cache_keys) - self.context = { + self._context = { "url_param": extra_cache.url_param, "current_user_id": extra_cache.current_user_id, "current_username": extra_cache.current_username, @@ -231,11 +231,11 @@ def __init__( "filter_values": filter_values, "form_data": {}, } - self.context.update(kwargs) - self.context.update(jinja_base_context) + self._context.update(kwargs) + self._context.update(jinja_base_context) if self.engine: - self.context[self.engine] = self - self.env = SandboxedEnvironment() + self._context[self.engine] = self + self._env = SandboxedEnvironment() def process_template(self, sql: str, **kwargs: Any) -> str: """Processes a sql template @@ -244,8 +244,8 @@ def process_template(self, sql: str, **kwargs: Any) -> str: >>> process_template(sql) "SELECT '2017-01-01T00:00:00'" """ - template = self.env.from_string(sql) - kwargs.update(self.context) + template = self._env.from_string(sql) + kwargs.update(self._context) return template.render(kwargs) @@ -288,20 +288,20 @@ def latest_partitions(self, table_name: str) -> Optional[List[str]]: from superset.db_engine_specs.presto import PrestoEngineSpec - table_name, schema = self._schema_table(table_name, self.schema) - return cast(PrestoEngineSpec, self.database.db_engine_spec).latest_partition( - table_name, schema, self.database + table_name, schema = self._schema_table(table_name, self._schema) + return cast(PrestoEngineSpec, self._database.db_engine_spec).latest_partition( + table_name, schema, self._database )[1] def latest_sub_partition(self, table_name: str, **kwargs: Any) -> Any: - table_name, schema = self._schema_table(table_name, self.schema) + table_name, schema = self._schema_table(table_name, self._schema) from superset.db_engine_specs.presto import PrestoEngineSpec return cast( - PrestoEngineSpec, self.database.db_engine_spec + PrestoEngineSpec, self._database.db_engine_spec ).latest_sub_partition( - table_name=table_name, schema=schema, database=self.database, **kwargs + table_name=table_name, schema=schema, database=self._database, **kwargs ) latest_partition = first_latest_partition diff --git a/superset/migrations/versions/e5ef6828ac4e_add_rls_filter_type_and_grouping_key.py b/superset/migrations/versions/e5ef6828ac4e_add_rls_filter_type_and_grouping_key.py new file mode 100644 index 0000000000000..01fcf60e93357 --- /dev/null +++ b/superset/migrations/versions/e5ef6828ac4e_add_rls_filter_type_and_grouping_key.py @@ -0,0 +1,58 @@ +# 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. +"""add rls filter type and grouping key + +Revision ID: e5ef6828ac4e +Revises: ae19b4ee3692 +Create Date: 2020-09-15 18:22:40.130985 + +""" + +# revision identifiers, used by Alembic. +revision = "e5ef6828ac4e" +down_revision = "ae19b4ee3692" + +import sqlalchemy as sa +from alembic import op + +from superset.utils import core as utils + + +def upgrade(): + with op.batch_alter_table("row_level_security_filters") as batch_op: + batch_op.add_column(sa.Column("filter_type", sa.VARCHAR(255), nullable=True)), + batch_op.add_column(sa.Column("group_key", sa.VARCHAR(255), nullable=True)), + batch_op.create_index( + op.f("ix_row_level_security_filters_filter_type"), + ["filter_type"], + unique=False, + ) + + bind = op.get_bind() + metadata = sa.MetaData(bind=bind) + filters = sa.Table("row_level_security_filters", metadata, autoload=True) + statement = filters.update().values( + filter_type=utils.RowLevelSecurityFilterType.REGULAR.value + ) + bind.execute(statement) + + +def downgrade(): + with op.batch_alter_table("row_level_security_filters") as batch_op: + batch_op.drop_index(op.f("ix_row_level_security_filters_filter_type"),) + batch_op.drop_column("filter_type") + batch_op.drop_column("group_key") diff --git a/superset/models/helpers.py b/superset/models/helpers.py index 105a727761ee4..fd55bba5ce27c 100644 --- a/superset/models/helpers.py +++ b/superset/models/helpers.py @@ -19,10 +19,9 @@ import logging import re from datetime import datetime, timedelta +from json.decoder import JSONDecodeError from typing import Any, Dict, List, Optional, Set, Union -# isort and pylint disagree, isort should win -# pylint: disable=ungrouped-imports import humanize import pandas as pd import pytz @@ -45,9 +44,7 @@ def json_to_dict(json_str: str) -> Dict[Any, Any]: if json_str: val = re.sub(",[ \t\r\n]+}", "}", json_str) - val = re.sub( - ",[ \t\r\n]+\]", "]", val # pylint: disable=anomalous-backslash-in-string - ) + val = re.sub(",[ \t\r\n]+\\]", "]", val) return json.loads(val) return {} @@ -89,6 +86,14 @@ def _unique_constrains(cls) -> List[Set[str]]: ) return unique + @classmethod + def parent_foreign_key_mappings(cls) -> Dict[str, str]: + """Get a mapping of foreign name to the local name of foreign keys""" + parent_rel = cls.__mapper__.relationships.get(cls.export_parent) + if parent_rel: + return {l.name: r.name for (l, r) in parent_rel.local_remote_pairs} + return {} + @classmethod def export_schema( cls, recursive: bool = True, include_parent_ref: bool = False @@ -135,7 +140,7 @@ def import_from_dict( """Import obj from a dictionary""" if sync is None: sync = [] - parent_refs = cls._parent_foreign_key_mappings() + parent_refs = cls.parent_foreign_key_mappings() export_fields = set(cls.export_fields) | set(parent_refs.keys()) new_children = {c: dict_rep[c] for c in cls.export_children if c in dict_rep} unique_constrains = cls._unique_constrains() @@ -217,9 +222,7 @@ def import_from_dict( # If children should get synced, delete the ones that did not # get updated. if child in sync and not is_new_obj: - back_refs = ( - child_class._parent_foreign_key_mappings() # pylint: disable=protected-access - ) + back_refs = child_class.parent_foreign_key_mappings() delete_filters = [ getattr(child_class, k) == getattr(obj, back_refs.get(k)) for k in back_refs.keys() @@ -306,11 +309,9 @@ def reset_ownership(self) -> None: self.created_by = None self.changed_by = None # flask global context might not exist (in cli or tests for example) - try: - if g.user: - self.owners = [g.user] - except Exception: # pylint: disable=broad-except - self.owners = [] + self.owners = [] + if g and hasattr(g, "user"): + self.owners = [g.user] @property def params_dict(self) -> Dict[Any, Any]: @@ -321,7 +322,7 @@ def template_params_dict(self) -> Dict[Any, Any]: return json_to_dict(self.template_params) # type: ignore -def _user_link(user: User) -> Union[Markup, str]: # pylint: disable=no-self-use +def _user_link(user: User) -> Union[Markup, str]: if not user: return "" url = "/superset/profile/{}/".format(user.username) @@ -424,7 +425,10 @@ class ExtraJSONMixin: def extra(self) -> Dict[str, Any]: try: return json.loads(self.extra_json) - except Exception: # pylint: disable=broad-except + except (TypeError, JSONDecodeError) as exc: + logger.error( + "Unable to load an extra json: %r. Leaving empty.", exc, exc_info=True + ) return {} def set_extra_json(self, extras: Dict[str, Any]) -> None: diff --git a/superset/security/manager.py b/superset/security/manager.py index 8607758263a11..e6f0b786f3714 100644 --- a/superset/security/manager.py +++ b/superset/security/manager.py @@ -38,7 +38,7 @@ ViewMenuModelView, ) from flask_appbuilder.widgets import ListWidget -from sqlalchemy import or_ +from sqlalchemy import and_, or_ from sqlalchemy.engine.base import Connection from sqlalchemy.orm.mapper import Mapper from sqlalchemy.orm.query import Query as SqlaQuery @@ -48,7 +48,7 @@ from superset.constants import RouteMethod, Security as SecurityConsts from superset.errors import ErrorLevel, SupersetError, SupersetErrorType from superset.exceptions import SupersetSecurityException -from superset.utils.core import DatasourceName +from superset.utils.core import DatasourceName, RowLevelSecurityFilterType if TYPE_CHECKING: from superset.common.query_context import QueryContext @@ -64,7 +64,7 @@ class SupersetSecurityListWidget(ListWidget): """ - Redeclaring to avoid circular imports + Redeclaring to avoid circular imports """ template = "superset/fab_overrides/list.html" @@ -72,8 +72,8 @@ class SupersetSecurityListWidget(ListWidget): class SupersetRoleListWidget(ListWidget): """ - Role model view from FAB already uses a custom list widget override - So we override the override + Role model view from FAB already uses a custom list widget override + So we override the override """ template = "superset/fab_overrides/list_role.html" @@ -1070,8 +1070,23 @@ def get_rls_filters( # pylint: disable=no-self-use .filter(assoc_user_role.c.user_id == g.user.id) .subquery() ) - filter_roles = ( + regular_filter_roles = ( self.get_session.query(RLSFilterRoles.c.rls_filter_id) + .join(RowLevelSecurityFilter) + .filter( + RowLevelSecurityFilter.filter_type + == RowLevelSecurityFilterType.REGULAR + ) + .filter(RLSFilterRoles.c.role_id.in_(user_roles)) + .subquery() + ) + base_filter_roles = ( + self.get_session.query(RLSFilterRoles.c.rls_filter_id) + .join(RowLevelSecurityFilter) + .filter( + RowLevelSecurityFilter.filter_type + == RowLevelSecurityFilterType.BASE + ) .filter(RLSFilterRoles.c.role_id.in_(user_roles)) .subquery() ) @@ -1082,10 +1097,25 @@ def get_rls_filters( # pylint: disable=no-self-use ) query = ( self.get_session.query( - RowLevelSecurityFilter.id, RowLevelSecurityFilter.clause + RowLevelSecurityFilter.id, + RowLevelSecurityFilter.group_key, + RowLevelSecurityFilter.clause, ) .filter(RowLevelSecurityFilter.id.in_(filter_tables)) - .filter(RowLevelSecurityFilter.id.in_(filter_roles)) + .filter( + or_( + and_( + RowLevelSecurityFilter.filter_type + == RowLevelSecurityFilterType.REGULAR, + RowLevelSecurityFilter.id.in_(regular_filter_roles), + ), + and_( + RowLevelSecurityFilter.filter_type + == RowLevelSecurityFilterType.BASE, + RowLevelSecurityFilter.id.notin_(base_filter_roles), + ), + ) + ) ) return query.all() return [] diff --git a/superset/tasks/alerts/observer.py b/superset/tasks/alerts/observer.py index 34ff6689583dc..482faedd805e1 100644 --- a/superset/tasks/alerts/observer.py +++ b/superset/tasks/alerts/observer.py @@ -48,7 +48,7 @@ def observe(alert_id: int, session: Session) -> Optional[str]: error_msg = validate_observer_result(df, alert.id, alert.label) - if not error_msg and df.to_records()[0][1] is not None: + if not error_msg and not df.empty and df.to_records()[0][1] is not None: value = float(df.to_records()[0][1]) observation = SQLObservation( @@ -74,9 +74,9 @@ def validate_observer_result( Returns an error message if the result is invalid. """ try: - assert ( - not sql_result.empty - ), f"Observer for alert <{alert_id}:{alert_label}> returned no rows" + if sql_result.empty: + # empty results are used for the not null validator + return None rows = sql_result.to_records() diff --git a/superset/tasks/schedules.py b/superset/tasks/schedules.py index 09a42145e8cd6..2cc1280f01085 100644 --- a/superset/tasks/schedules.py +++ b/superset/tasks/schedules.py @@ -46,6 +46,7 @@ from selenium.common.exceptions import WebDriverException from selenium.webdriver import chrome, firefox from selenium.webdriver.remote.webdriver import WebDriver +from sqlalchemy import func from sqlalchemy.exc import NoSuchColumnError, ResourceClosedError from sqlalchemy.orm import Session @@ -200,12 +201,21 @@ def _get_url_path(view: str, user_friendly: bool = False, **kwargs: Any) -> str: return urllib.parse.urljoin(str(base_url), url_for(view, **kwargs)) -def create_webdriver() -> WebDriver: - return WebDriverProxy(driver_type=config["WEBDRIVER_TYPE"]).auth(get_reports_user()) +def create_webdriver(session: Session) -> WebDriver: + return WebDriverProxy(driver_type=config["WEBDRIVER_TYPE"]).auth( + get_reports_user(session) + ) -def get_reports_user() -> "User": - return security_manager.find_user(config["EMAIL_REPORTS_USER"]) +def get_reports_user(session: Session) -> "User": + return ( + session.query(security_manager.user_model) + .filter( + func.lower(security_manager.user_model.username) + == func.lower(config["EMAIL_REPORTS_USER"]) + ) + .one() + ) def destroy_webdriver( @@ -249,7 +259,7 @@ def deliver_dashboard( # pylint: disable=too-many-locals ) # Create a driver, fetch the page, wait for the page to render - driver = create_webdriver() + driver = create_webdriver(session) window = config["WEBDRIVER_WINDOW"]["dashboard"] driver.set_window_size(*window) driver.get(dashboard_url) @@ -303,7 +313,9 @@ def deliver_dashboard( # pylint: disable=too-many-locals ) -def _get_slice_data(slc: Slice, delivery_type: EmailDeliveryType) -> ReportContent: +def _get_slice_data( + slc: Slice, delivery_type: EmailDeliveryType, session: Session +) -> ReportContent: slice_url = _get_url_path( "Superset.explore_json", csv="true", form_data=json.dumps({"slice_id": slc.id}) ) @@ -315,7 +327,7 @@ def _get_slice_data(slc: Slice, delivery_type: EmailDeliveryType) -> ReportConte # Login on behalf of the "reports" user in order to get cookies to deal with auth auth_cookies = machine_auth_provider_factory.instance.get_auth_cookies( - get_reports_user() + get_reports_user(session) ) # Build something like "session=cool_sess.val;other-cookie=awesome_other_cookie" cookie_str = ";".join([f"{key}={val}" for key, val in auth_cookies.items()]) @@ -384,10 +396,10 @@ def _get_slice_screenshot(slice_id: int, session: Session) -> ScreenshotData: def _get_slice_visualization( - slc: Slice, delivery_type: EmailDeliveryType + slc: Slice, delivery_type: EmailDeliveryType, session: Session ) -> ReportContent: # Create a driver, fetch the page, wait for the page to render - driver = create_webdriver() + driver = create_webdriver(session) window = config["WEBDRIVER_WINDOW"]["slice"] driver.set_window_size(*window) @@ -438,9 +450,9 @@ def deliver_slice( # pylint: disable=too-many-arguments slc = session.query(Slice).filter_by(id=slice_id).one() if email_format == SliceEmailReportFormat.data: - report_content = _get_slice_data(slc, delivery_type) + report_content = _get_slice_data(slc, delivery_type, session) elif email_format == SliceEmailReportFormat.visualization: - report_content = _get_slice_visualization(slc, delivery_type) + report_content = _get_slice_visualization(slc, delivery_type, session) else: raise RuntimeError("Unknown email report format") diff --git a/superset/templates/superset/models/rls/list.html b/superset/templates/superset/models/rls/list.html new file mode 100644 index 0000000000000..905ee6d305b1a --- /dev/null +++ b/superset/templates/superset/models/rls/list.html @@ -0,0 +1,96 @@ +{# + 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. +#} +{% extends 'appbuilder/general/widgets/base_list.html' %} +{% import 'appbuilder/general/lib.html' as lib %} + + {% block begin_content scoped %} +
+ + {% endblock %} + + {% block begin_loop_header scoped %} + + + {% if actions %} + + {% endif %} + + {% if can_show or can_edit or can_delete %} + + {% endif %} + + {% for item in include_columns %} + {% if item in order_columns %} + {% set res = item | get_link_order(modelview_name) %} + {% if res == 2 %} + + {% elif res == 1 %} + + {% else %} + + {% endif %} + {% else %} + + {% endif %} + {% endfor %} + + + {% endblock %} + + {% block begin_loop_values %} + {% for item in value_columns %} + {% set pk = pks[loop.index-1] %} + + {% if actions %} + + {% endif %} + {% if can_show or can_edit or can_delete %} + + {% endif %} + {% for value in include_columns %} + + {% endfor %} + + {% endfor %} + {% endblock %} + + {% block end_content scoped %} +
+ + {{label_columns.get(item)}} + {{label_columns.get(item)}} + {{label_columns.get(item)}} + {{label_columns.get(item)}}
+ +
+ {{ lib.btn_crud(can_show, can_edit, can_delete, pk, modelview_name, filters) }} +
+ {% if value == "roles" and item["filter_type"] == "Base" and not item[value] %} + All + {% elif value == "roles" and item["filter_type"] == 'Base' %} + Not {{ item[value] }} + {% elif value == "roles" and item["filter_type"] == 'Regular' and not item[value] %} + None + {% elif value == "group_key" and item[value] == None %} + {% else %} + {{ item[value] }} + {% endif %} +
+
+ {% endblock %} diff --git a/superset/utils/core.py b/superset/utils/core.py index 5ccb74aa99fa1..bfd9742c846ed 100644 --- a/superset/utils/core.py +++ b/superset/utils/core.py @@ -1040,24 +1040,7 @@ def backend() -> str: def is_adhoc_metric(metric: Metric) -> bool: - if not isinstance(metric, dict): - return False - metric = cast(Dict[str, Any], metric) - return bool( - ( - ( - metric.get("expressionType") == AdhocMetricExpressionType.SIMPLE - and metric.get("column") - and cast(Dict[str, Any], metric["column"]).get("column_name") - and metric.get("aggregate") - ) - or ( - metric.get("expressionType") == AdhocMetricExpressionType.SQL - and metric.get("sqlExpression") - ) - ) - and metric.get("label") - ) + return isinstance(metric, dict) def get_metric_name(metric: Metric) -> str: @@ -1564,3 +1547,8 @@ class PostProcessingContributionOrientation(str, Enum): class AdhocMetricExpressionType(str, Enum): SIMPLE = "SIMPLE" SQL = "SQL" + + +class RowLevelSecurityFilterType(str, Enum): + REGULAR = "Regular" + BASE = "Base" diff --git a/tests/alerts_tests.py b/tests/alerts_tests.py index 81c94b00bfcd9..b09ac930e7172 100644 --- a/tests/alerts_tests.py +++ b/tests/alerts_tests.py @@ -128,11 +128,11 @@ def test_alert_observer(setup_database): assert alert3.sql_observer[0].observations[-1].value is None assert alert3.sql_observer[0].observations[-1].error_msg is None - # Test SQLObserver with empty SQL return + # Test SQLObserver with empty SQL return, expected alert4 = create_alert(dbsession, "SELECT first FROM test_table WHERE first = -1") observe(alert4.id, dbsession) assert alert4.sql_observer[0].observations[-1].value is None - assert alert4.sql_observer[0].observations[-1].error_msg is not None + assert alert4.sql_observer[0].observations[-1].error_msg is None # Test SQLObserver with str result alert5 = create_alert(dbsession, "SELECT 'test_string' as string_value") diff --git a/tests/db_engine_specs/presto_tests.py b/tests/db_engine_specs/presto_tests.py index 3a0346bfe2591..7045fc34ecb05 100644 --- a/tests/db_engine_specs/presto_tests.py +++ b/tests/db_engine_specs/presto_tests.py @@ -511,3 +511,6 @@ def test_get_sqla_column_type(self): sqla_type = PrestoEngineSpec.get_sqla_column_type("integer") assert isinstance(sqla_type, types.Integer) + + sqla_type = PrestoEngineSpec.get_sqla_column_type(None) + assert sqla_type is None diff --git a/tests/schedules_test.py b/tests/schedules_test.py index 88b6d1f924d1f..b18007e2f77a3 100644 --- a/tests/schedules_test.py +++ b/tests/schedules_test.py @@ -171,7 +171,7 @@ def test_create_driver(self, mock_driver_class): mock_driver_class.return_value = mock_driver mock_driver.find_elements_by_id.side_effect = [True, False] - create_webdriver() + create_webdriver(db.session) mock_driver.add_cookie.assert_called_once() @patch("superset.tasks.schedules.firefox.webdriver.WebDriver") diff --git a/tests/security_tests.py b/tests/security_tests.py index fdb6be7c33491..29926a7ee9610 100644 --- a/tests/security_tests.py +++ b/tests/security_tests.py @@ -16,6 +16,7 @@ # under the License. # isort:skip_file import inspect +import re import unittest from unittest.mock import Mock, patch @@ -1015,70 +1016,116 @@ class TestRowLevelSecurity(SupersetTestCase): """ rls_entry = None + query_obj = dict( + groupby=[], + metrics=[], + filter=[], + is_timeseries=False, + columns=["value"], + granularity=None, + from_dttm=None, + to_dttm=None, + extras={}, + ) + NAME_AB_ROLE = "NameAB" + NAME_Q_ROLE = "NameQ" + NAMES_A_REGEX = re.compile(r"name like 'A%'") + NAMES_B_REGEX = re.compile(r"name like 'B%'") + NAMES_Q_REGEX = re.compile(r"name like 'Q%'") + BASE_FILTER_REGEX = re.compile(r"gender = 'boy'") def setUp(self): session = db.session - # Create the RowLevelSecurityFilter - self.rls_entry = RowLevelSecurityFilter() - self.rls_entry.tables.extend( + # Create roles + security_manager.add_role(self.NAME_AB_ROLE) + security_manager.add_role(self.NAME_Q_ROLE) + gamma_user = security_manager.find_user(username="gamma") + gamma_user.roles.append(security_manager.find_role(self.NAME_AB_ROLE)) + gamma_user.roles.append(security_manager.find_role(self.NAME_Q_ROLE)) + self.create_user_with_roles("NoRlsRoleUser", ["Gamma"]) + session.commit() + + # Create regular RowLevelSecurityFilter (energy_usage, unicode_test) + self.rls_entry1 = RowLevelSecurityFilter() + self.rls_entry1.tables.extend( session.query(SqlaTable) .filter(SqlaTable.table_name.in_(["energy_usage", "unicode_test"])) .all() ) - self.rls_entry.clause = "value > {{ cache_key_wrapper(1) }}" - self.rls_entry.roles.append( - security_manager.find_role("Gamma") - ) # db.session.query(Role).filter_by(name="Gamma").first()) - self.rls_entry.roles.append(security_manager.find_role("Alpha")) - db.session.add(self.rls_entry) + self.rls_entry1.filter_type = "Regular" + self.rls_entry1.clause = "value > {{ cache_key_wrapper(1) }}" + self.rls_entry1.group_key = None + self.rls_entry1.roles.append(security_manager.find_role("Gamma")) + self.rls_entry1.roles.append(security_manager.find_role("Alpha")) + db.session.add(self.rls_entry1) + + # Create regular RowLevelSecurityFilter (birth_names name starts with A or B) + self.rls_entry2 = RowLevelSecurityFilter() + self.rls_entry2.tables.extend( + session.query(SqlaTable) + .filter(SqlaTable.table_name.in_(["birth_names"])) + .all() + ) + self.rls_entry2.filter_type = "Regular" + self.rls_entry2.clause = "name like 'A%' or name like 'B%'" + self.rls_entry2.group_key = "name" + self.rls_entry2.roles.append(security_manager.find_role("NameAB")) + db.session.add(self.rls_entry2) + + # Create Regular RowLevelSecurityFilter (birth_names name starts with Q) + self.rls_entry3 = RowLevelSecurityFilter() + self.rls_entry3.tables.extend( + session.query(SqlaTable) + .filter(SqlaTable.table_name.in_(["birth_names"])) + .all() + ) + self.rls_entry3.filter_type = "Regular" + self.rls_entry3.clause = "name like 'Q%'" + self.rls_entry3.group_key = "name" + self.rls_entry3.roles.append(security_manager.find_role("NameQ")) + db.session.add(self.rls_entry3) + + # Create Base RowLevelSecurityFilter (birth_names boys) + self.rls_entry4 = RowLevelSecurityFilter() + self.rls_entry4.tables.extend( + session.query(SqlaTable) + .filter(SqlaTable.table_name.in_(["birth_names"])) + .all() + ) + self.rls_entry4.filter_type = "Base" + self.rls_entry4.clause = "gender = 'boy'" + self.rls_entry4.group_key = "gender" + self.rls_entry4.roles.append(security_manager.find_role("Admin")) + db.session.add(self.rls_entry4) db.session.commit() def tearDown(self): session = db.session - session.delete(self.rls_entry) + session.delete(self.rls_entry1) + session.delete(self.rls_entry2) + session.delete(self.rls_entry3) + session.delete(self.rls_entry4) + session.delete(security_manager.find_role("NameAB")) + session.delete(security_manager.find_role("NameQ")) + session.delete(self.get_user("NoRlsRoleUser")) session.commit() - # Do another test to make sure it doesn't alter another query - def test_rls_filter_alters_query(self): - g.user = self.get_user( - username="alpha" - ) # self.login() doesn't actually set the user + def test_rls_filter_alters_energy_query(self): + g.user = self.get_user(username="alpha") tbl = self.get_table_by_name("energy_usage") - query_obj = dict( - groupby=[], - metrics=[], - filter=[], - is_timeseries=False, - columns=["value"], - granularity=None, - from_dttm=None, - to_dttm=None, - extras={}, - ) - sql = tbl.get_query_str(query_obj) - assert tbl.get_extra_cache_keys(query_obj) == [1] + sql = tbl.get_query_str(self.query_obj) + assert tbl.get_extra_cache_keys(self.query_obj) == [1] assert "value > 1" in sql - def test_rls_filter_doesnt_alter_query(self): + def test_rls_filter_doesnt_alter_energy_query(self): g.user = self.get_user( username="admin" ) # self.login() doesn't actually set the user tbl = self.get_table_by_name("energy_usage") - query_obj = dict( - groupby=[], - metrics=[], - filter=[], - is_timeseries=False, - columns=["value"], - granularity=None, - from_dttm=None, - to_dttm=None, - extras={}, - ) - sql = tbl.get_query_str(query_obj) - assert tbl.get_extra_cache_keys(query_obj) == [] + sql = tbl.get_query_str(self.query_obj) + assert tbl.get_extra_cache_keys(self.query_obj) == [] assert "value > 1" not in sql def test_multiple_table_filter_alters_another_tables_query(self): @@ -1086,17 +1133,41 @@ def test_multiple_table_filter_alters_another_tables_query(self): username="alpha" ) # self.login() doesn't actually set the user tbl = self.get_table_by_name("unicode_test") - query_obj = dict( - groupby=[], - metrics=[], - filter=[], - is_timeseries=False, - columns=["value"], - granularity=None, - from_dttm=None, - to_dttm=None, - extras={}, - ) - sql = tbl.get_query_str(query_obj) - assert tbl.get_extra_cache_keys(query_obj) == [1] + sql = tbl.get_query_str(self.query_obj) + assert tbl.get_extra_cache_keys(self.query_obj) == [1] assert "value > 1" in sql + + def test_rls_filter_alters_gamma_birth_names_query(self): + g.user = self.get_user(username="gamma") + tbl = self.get_table_by_name("birth_names") + sql = tbl.get_query_str(self.query_obj) + + # establish that the filters are grouped together correctly with + # ANDs, ORs and parens in the correct place + assert ( + "WHERE ((name like 'A%'\n or name like 'B%')\n OR (name like 'Q%'))\n AND (gender = 'boy');" + in sql + ) + + def test_rls_filter_alters_no_role_user_birth_names_query(self): + g.user = self.get_user(username="NoRlsRoleUser") + tbl = self.get_table_by_name("birth_names") + sql = tbl.get_query_str(self.query_obj) + + # gamma's filters should not be present query + assert not self.NAMES_A_REGEX.search(sql) + assert not self.NAMES_B_REGEX.search(sql) + assert not self.NAMES_Q_REGEX.search(sql) + # base query should be present + assert self.BASE_FILTER_REGEX.search(sql) + + def test_rls_filter_doesnt_alter_admin_birth_names_query(self): + g.user = self.get_user(username="admin") + tbl = self.get_table_by_name("birth_names") + sql = tbl.get_query_str(self.query_obj) + + # no filters are applied for admin user + assert not self.NAMES_A_REGEX.search(sql) + assert not self.NAMES_B_REGEX.search(sql) + assert not self.NAMES_Q_REGEX.search(sql) + assert not self.BASE_FILTER_REGEX.search(sql) diff --git a/tests/superset_test_custom_template_processors.py b/tests/superset_test_custom_template_processors.py index b3534814786a7..124c73931f4e5 100644 --- a/tests/superset_test_custom_template_processors.py +++ b/tests/superset_test_custom_template_processors.py @@ -42,7 +42,7 @@ def process_template(self, sql: str, **kwargs) -> str: # Add custom macros functions. macros = {"DATE": partial(DATE, datetime.utcnow())} # type: Dict[str, Any] # Update with macros defined in context and kwargs. - macros.update(self.context) + macros.update(self._context) macros.update(kwargs) def replacer(match):