From a57ed76e728958447e1f05445519ff5c0774031b Mon Sep 17 00:00:00 2001 From: Reese <10563996+reesercollins@users.noreply.github.com> Date: Wed, 8 Jun 2022 10:02:58 -0400 Subject: [PATCH 01/71] Fix faulty datetime parser regex (#20290) --- .../components/controls/DateFilterControl/utils/dateParser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset-frontend/src/explore/components/controls/DateFilterControl/utils/dateParser.ts b/superset-frontend/src/explore/components/controls/DateFilterControl/utils/dateParser.ts index 0d259f8c80d02..e4863665bffb4 100644 --- a/superset-frontend/src/explore/components/controls/DateFilterControl/utils/dateParser.ts +++ b/superset-frontend/src/explore/components/controls/DateFilterControl/utils/dateParser.ts @@ -35,7 +35,7 @@ import { SEVEN_DAYS_AGO, MIDNIGHT, MOMENT_FORMAT } from './constants'; * @see: https://www.w3.org/TR/NOTE-datetime */ const iso8601 = String.raw`\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?(?:(?:[+-]\d\d:\d\d)|Z)?`; -const datetimeConstant = String.raw`TODAY|NOW`; +const datetimeConstant = String.raw`(?:TODAY|NOW)`; const grainValue = String.raw`[+-]?[1-9][0-9]*`; const grain = String.raw`YEAR|QUARTER|MONTH|WEEK|DAY|HOUR|MINUTE|SECOND`; const CUSTOM_RANGE_EXPRESSION = RegExp( From 80be1ce6575fdcef7f47b8136c9b91bd7f9ad5d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Boisseau-Sierra?= <37387755+EBoisseauSierra@users.noreply.github.com> Date: Wed, 8 Jun 2022 15:08:37 +0100 Subject: [PATCH 02/71] docs: Detail how to use Jinja parameters (#20308) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We expand the example to show how to use the `from_dttm` and `to_dttm` Jinja parameters in logic blocks (e.g. `{% if … %}`) and underline when to use the double braces and when not to. All this because, well, *one* could lost quite an extensive amount of time figuring this all out (-_-") Signed-off-by: Étienne Boisseau-Sierra --- docs/docs/installation/sql-templating.mdx | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/docs/installation/sql-templating.mdx b/docs/docs/installation/sql-templating.mdx index 905f99cb9f5de..09373d8999d06 100644 --- a/docs/docs/installation/sql-templating.mdx +++ b/docs/docs/installation/sql-templating.mdx @@ -33,6 +33,26 @@ For example, to add a time range to a virtual dataset, you can write the followi SELECT * from tbl where dttm_col > '{{ from_dttm }}' and dttm_col < '{{ to_dttm }}' ``` +You can also use [Jinja's logic](https://jinja.palletsprojects.com/en/2.11.x/templates/#tests) +to make your query robust to clearing the timerange filter: + +```sql +SELECT * +FROM tbl +WHERE ( + {% if from_dttm is not none %} + dttm_col > '{{ from_dttm }}' AND + {% endif %} + {% if to_dttm is not none %} + dttm_col < '{{ to_dttm }}' AND + {% endif %} + true +) +``` + +Note how the Jinja parameters are called within double brackets in the query, and without in the +logic blocks. + To add custom functionality to the Jinja context, you need to overload the default Jinja context in your environment by defining the `JINJA_CONTEXT_ADDONS` in your superset configuration (`superset_config.py`). Objects referenced in this dictionary are made available for users to use From b6c11f2b971abd45281dafa0ac0b105e9d3ba6db Mon Sep 17 00:00:00 2001 From: "Hugh A. Miles II" Date: Wed, 8 Jun 2022 16:24:36 +0200 Subject: [PATCH 03/71] fix: Alpha are unable to perform a second modification to a Dataset when in Explore (#20296) * handle payload coming back from save * address concerns --- .../components/controls/DatasourceControl/index.jsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx b/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx index 3d6ea2fdd2662..f3d4f8aabe0aa 100644 --- a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx +++ b/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx @@ -198,9 +198,10 @@ class DatasourceControl extends React.PureComponent { const isSqlSupported = datasource.type === 'table'; const { user } = this.props; - const allowEdit = - datasource.owners.map(o => o.id).includes(user.userId) || - isUserAdmin(user); + const allowEdit = datasource.owners + .map(o => o.id || o.value) + .includes(user.userId); + isUserAdmin(user); const editText = t('Edit dataset'); From 0238492df71a72a54601c43d4075b65551d053a0 Mon Sep 17 00:00:00 2001 From: "Michael S. Molina" <70410625+michael-s-molina@users.noreply.github.com> Date: Wed, 8 Jun 2022 13:32:58 -0300 Subject: [PATCH 04/71] docs: Updates CHANGELOG.md with 1.5.1 fixes (#20307) --- CHANGELOG.md | 59 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 42 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb5151352a3e2..ac47e129cf7a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,8 +16,25 @@ KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --> + ## Change Log +### 1.5.1 (Thu May 26 14:45:20 2022 +0300) + +**Fixes** + +- [#19685](https://github.com/apache/superset/pull/19685) fix: login button does not render (@villebro) +- [#20181](https://github.com/apache/superset/pull/20181) fix(temporary-cache): when user is anonymous (@villebro) +- [#20091](https://github.com/apache/superset/pull/20091) fix: "Week Staring Monday" time grain for BigQuery (@ramunas-omnisend) +- [#20135](https://github.com/apache/superset/pull/20135) fix: Allow dataset owners to see their datasets (@cccs-tom) +- [#20123](https://github.com/apache/superset/pull/20123) fix(presto,trino): use correct literal dttm separator (@villebro) +- [#20077](https://github.com/apache/superset/pull/20077) fix(generic-axes): apply contribution before flatten (@villebro) +- [#19970](https://github.com/apache/superset/pull/19970) fix: Athena timestamp literal format (@thinhnd2104) +- [#20055](https://github.com/apache/superset/pull/20055) fix(plugin-chart-echarts): support adhoc x-axis (@villebro) +- [#18873](https://github.com/apache/superset/pull/18873) fix(sqllab/charts): casting from timestamp[us] to timestamp[ns] would result in out of bounds timestamp (@yeachan153) +- [#19917](https://github.com/apache/superset/pull/19917) fix(sqla): replace custom dttm type with literal_column (@villebro) +- [#19854](https://github.com/apache/superset/pull/19854) fix: Alpha should not be able to edit datasets that they don't own (@hughhhh) + ### 1.5.0 (Fri Apr 22 17:23:30 2022 -0400) - **Database Migrations** @@ -37,7 +54,7 @@ under the License. - [#17928](https://github.com/apache/superset/pull/17928) fix: force_screenshot migration (@betodealmeida) - [#17853](https://github.com/apache/superset/pull/17853) feat: add force option to report screenshots (@betodealmeida) - [#17803](https://github.com/apache/superset/pull/17803) feat(plugin-chart-pivot-table): support series limit (@kgabryje) -- [#17587](https://github.com/apache/superset/pull/17587) chore(explore): Migrate BigNumber to v1 api [ID-28][ID-55] (@kgabryje) +- [#17587](https://github.com/apache/superset/pull/17587) chore(explore): Migrate BigNumber to v1 api [ID-28][id-55] (@kgabryje) - [#17360](https://github.com/apache/superset/pull/17360) fix: Change datatype of column type in BaseColumn to allow larger datatype names for complexed columns (@cccs-joel) - [#17728](https://github.com/apache/superset/pull/17728) fix: migration out-of-scope bind (@betodealmeida) - [#17604](https://github.com/apache/superset/pull/17604) feat(alerts): Column for select tabs to send (@m-ajay) @@ -46,6 +63,7 @@ under the License. - [#16756](https://github.com/apache/superset/pull/16756) refactor: Repeated boilerplate code between upload to database forms (@exemplary-citizen) **Features** + - [#19776](https://github.com/apache/superset/pull/19776) feat: add renameOperator (@zhaoyongjie) - [#19328](https://github.com/apache/superset/pull/19328) feat(sql lab): enable ACE editor search in SQL editors (@diegomedina248) - [#19454](https://github.com/apache/superset/pull/19454) feat: improve adhoc SQL validation (@betodealmeida) @@ -97,7 +115,7 @@ under the License. - [#18679](https://github.com/apache/superset/pull/18679) feat(explore): Implement data table empty states (@kgabryje) - [#18652](https://github.com/apache/superset/pull/18652) feat: Introduce a library for embedded iframe <-> host communication (@suddjian) - [#18676](https://github.com/apache/superset/pull/18676) feat: Implement EmptyState components (@kgabryje) -- [#18653](https://github.com/apache/superset/pull/18653) feat: add prop to `setDBEngine` in DatabaseModal (@hughhhh) +- [#18653](https://github.com/apache/superset/pull/18653) feat: add prop to `setDBEngine` in DatabaseModal (@hughhhh) - [#18642](https://github.com/apache/superset/pull/18642) feat(Helm): Redis with password supported in helm charts and redis chart version updated (@wiktor2200) - [#18649](https://github.com/apache/superset/pull/18649) feat(helm): allow to customize init image (@avakarev) - [#18626](https://github.com/apache/superset/pull/18626) feat: editable title xl certified badge (@opus-42) @@ -175,6 +193,7 @@ under the License. - [#17001](https://github.com/apache/superset/pull/17001) feat(linting): restrict direct use of supersetTheme in favor of ThemeProvider (@rusackas) **Fixes** + - [#19448](https://github.com/apache/superset/pull/19448) fix(sql lab): when editing a saved query, the status is lost when switching tabs (@diegomedina248) - [#19806](https://github.com/apache/superset/pull/19806) fix(plugin-chart-table): Fix display of column config in table chart (@kgabryje) - [#19802](https://github.com/apache/superset/pull/19802) fix: lost renameOperator in mixed timeseries chart (@zhaoyongjie) @@ -248,7 +267,7 @@ under the License. - [#19121](https://github.com/apache/superset/pull/19121) fix(dashboard): scrolling table viz overlaps next chart (@diegomedina248) - [#19023](https://github.com/apache/superset/pull/19023) fix: Add perm for showing DBC-UI in Global Nav (@hughhhh) - [#19080](https://github.com/apache/superset/pull/19080) fix(dashboard-edge-cutting): make to be not cut without Filter (@prosdev0107) -- [#19110](https://github.com/apache/superset/pull/19110) fix: cache key with guest token rls (@lilykuang) +- [#19110](https://github.com/apache/superset/pull/19110) fix: cache key with guest token rls (@lilykuang) - [#19095](https://github.com/apache/superset/pull/19095) fix(dashboard): Empty states overflowing small chart containers (@kgabryje) - [#18947](https://github.com/apache/superset/pull/18947) fix(plugin-chart-echarts): make to allow the custome of x & y axis title margin i… (@prosdev0107) - [#19088](https://github.com/apache/superset/pull/19088) fix(dashboard): import handle missing excluded charts (@villebro) @@ -336,7 +355,7 @@ under the License. - [#18575](https://github.com/apache/superset/pull/18575) fix: superset-doc.yaml workflow to not be immutable on deploy (@hughhhh) - [#18182](https://github.com/apache/superset/pull/18182) fix: new alert should have force_screenshot be true (@graceguo-supercat) - [#18252](https://github.com/apache/superset/pull/18252) fix(plugin-chart-echarts): fix forecasts on verbose metrics (@villebro) -- [#18240](https://github.com/apache/superset/pull/18240) fix(teradata): LIMIT syntax (@dmcnulla) +- [#18240](https://github.com/apache/superset/pull/18240) fix(teradata): LIMIT syntax (@dmcnulla) - [#18224](https://github.com/apache/superset/pull/18224) fix(alert): remove extra < character in email report (@graceguo-supercat) - [#18201](https://github.com/apache/superset/pull/18201) fix: Build scripts (@geido) - [#18219](https://github.com/apache/superset/pull/18219) fix: Add mexico back to country map (@etr2460) @@ -373,7 +392,7 @@ under the License. - [#17388](https://github.com/apache/superset/pull/17388) fix(sqla): Adhere to series limit ordering for pre-query (@john-bodley) - [#17999](https://github.com/apache/superset/pull/17999) fix(helm): pin correct psycopg2 version (@villebro) - [#17988](https://github.com/apache/superset/pull/17988) fix: Keep Report modal open when there's an error (@lyndsiWilliams) -- [#17985](https://github.com/apache/superset/pull/17985) fix: dashboard full screen layout (@pkdotson) +- [#17985](https://github.com/apache/superset/pull/17985) fix: dashboard full screen layout (@pkdotson) - [#17931](https://github.com/apache/superset/pull/17931) fix(sqllab): Dancing Tooltip in SQL editor dropdown (@lyndsiWilliams) - [#17974](https://github.com/apache/superset/pull/17974) fix: null dates in table chart (@etr2460) - [#17878](https://github.com/apache/superset/pull/17878) fix: Returns 404 instead of 500 for unknown dashboard filter state keys (@michael-s-molina) @@ -385,7 +404,7 @@ under the License. - [#17918](https://github.com/apache/superset/pull/17918) fix(cypress): flake cypress test case (@zhaoyongjie) - [#17920](https://github.com/apache/superset/pull/17920) fix(helm): service account apiVersion indentation (@wiktor2200) - [#17877](https://github.com/apache/superset/pull/17877) fix(translation): include babel-compile in Dockerfile (#17876) (@hbruch) -- [#17872](https://github.com/apache/superset/pull/17872) fix(explore): simple tab content input problem in the filter control (@stephenLYZ) +- [#17872](https://github.com/apache/superset/pull/17872) fix(explore): simple tab content input problem in the filter control (@stephenLYZ) - [#17887](https://github.com/apache/superset/pull/17887) fix: Removes duplicated import in dashboard filter state tests (@michael-s-molina) - [#17885](https://github.com/apache/superset/pull/17885) fix: tests can failed on different order executions (@ofekisr) - [#17886](https://github.com/apache/superset/pull/17886) fix: failed mypy in master branch (@ofekisr) @@ -409,12 +428,12 @@ under the License. - [#17768](https://github.com/apache/superset/pull/17768) fix: change 401 response to a 403 for Security Exceptions (@rusackas) - [#17760](https://github.com/apache/superset/pull/17760) fix: miss-spelling on CONTRIBUTING.md line 1351 (@MayUWish) - [#17765](https://github.com/apache/superset/pull/17765) fix(plugin-chart-table): sort alphanumeric columns case insensitive (@kgabryje) -- [#17730](https://github.com/apache/superset/pull/17730) fix: add __init__.py to key_value (@bkyryliuk) +- [#17730](https://github.com/apache/superset/pull/17730) fix: add **init**.py to key_value (@bkyryliuk) - [#17727](https://github.com/apache/superset/pull/17727) fix: local warning in the frontend development (@stephenLYZ) - [#17738](https://github.com/apache/superset/pull/17738) fix: column extra in import/export (@betodealmeida) - [#17748](https://github.com/apache/superset/pull/17748) fix: import DB errors (@betodealmeida) - [#17741](https://github.com/apache/superset/pull/17741) fix: import dashboard stale filter_scopes (@betodealmeida) -- [#17649](https://github.com/apache/superset/pull/17649) fix(Mixed Timeseries Chart): Custom Metric Label (@Yahyakiani) +- [#17649](https://github.com/apache/superset/pull/17649) fix(Mixed Timeseries Chart): Custom Metric Label (@Yahyakiani) - [#17732](https://github.com/apache/superset/pull/17732) fix: import dash with missing immune ID (@betodealmeida) - [#17713](https://github.com/apache/superset/pull/17713) fix(postgres): remove redundant tz factory (@villebro) - [#17711](https://github.com/apache/superset/pull/17711) fix(explore): don't apply time range filter to Samples table (@kgabryje) @@ -437,7 +456,7 @@ under the License. - [#17600](https://github.com/apache/superset/pull/17600) fix: Ch31968query context (@AAfghahi) - [#17547](https://github.com/apache/superset/pull/17547) fix: fix text overflow in toast (@pkdotson) - [#17542](https://github.com/apache/superset/pull/17542) fix: Visualizations don't load when using keyboard shortcuts (@michael-s-molina) -- [#17539](https://github.com/apache/superset/pull/17539) fix(superset.cli): superset cli group doesn't support superset extension app (@ofekisr) +- [#17539](https://github.com/apache/superset/pull/17539) fix(superset.cli): superset cli group doesn't support superset extension app (@ofekisr) - [#14512](https://github.com/apache/superset/pull/14512) fix: update kubernetes.mdx (@shicholas) - [#17527](https://github.com/apache/superset/pull/17527) fix: RBAC for `can_export` for any resource (@hughhhh) - [#17555](https://github.com/apache/superset/pull/17555) fix(lint): remove redis xadd type ignore (@villebro) @@ -447,7 +466,7 @@ under the License. - [#15182](https://github.com/apache/superset/pull/15182) fix: hiding HiddenControl inputs for real, so they don't add height (@rusackas) - [#17511](https://github.com/apache/superset/pull/17511) fix: Dashboard access when DASHBOARD_RBAC is disabled (@michael-s-molina) - [#16799](https://github.com/apache/superset/pull/16799) fix: Bulk update Spanish translations (@dreglad) -- [#14302](https://github.com/apache/superset/pull/14302) fix(hive): Update _latest_partition_from_df in HiveEngineSpec to work on tables with multiple indexes (@codenamelxl) +- [#14302](https://github.com/apache/superset/pull/14302) fix(hive): Update \_latest_partition_from_df in HiveEngineSpec to work on tables with multiple indexes (@codenamelxl) - [#17458](https://github.com/apache/superset/pull/17458) fix: Always use temporal type for dttm columns [ID-2] (@kgabryje) - [#17470](https://github.com/apache/superset/pull/17470) fix(presto): expand data with null item (@ganczarek) - [#15254](https://github.com/apache/superset/pull/15254) fix: feature flags typing (@dpgaspar) @@ -513,6 +532,7 @@ under the License. - [#16838](https://github.com/apache/superset/pull/16838) fix: remove duplicate line in docstring (@exemplary-citizen) **Others** + - [#19732](https://github.com/apache/superset/pull/19732) chore: Clean redundant dependency from useMemo dep array (@kgabryje) - [#19636](https://github.com/apache/superset/pull/19636) chore: skip SIP-68 shadow writing for LTS (@ktmud) - [#19530](https://github.com/apache/superset/pull/19530) docs: release notes for 1.5 (@villebro) @@ -652,7 +672,7 @@ under the License. - [#18559](https://github.com/apache/superset/pull/18559) chore: Update Docusaurus dependencies (@geido) - [#18229](https://github.com/apache/superset/pull/18229) chore: Reference Github code in Docusaurus (@geido) - [#18251](https://github.com/apache/superset/pull/18251) chore(tests): migrate mssql tests to pytest (@villebro) -- [#18188](https://github.com/apache/superset/pull/18188) refactor: upgrade ControlHeader to TSX & FC and add storybook (@ad-m) +- [#18188](https://github.com/apache/superset/pull/18188) refactor: upgrade ControlHeader to TSX & FC and add storybook (@ad-m) - [#18230](https://github.com/apache/superset/pull/18230) chore: Docusaurus throw on broken links (@geido) - [#18170](https://github.com/apache/superset/pull/18170) refactor: extract json_required view decorator (@ad-m) - [#17926](https://github.com/apache/superset/pull/17926) refactor: sqleditorleftbar to typescript (@JosueLugaro) @@ -679,7 +699,7 @@ under the License. - [#17569](https://github.com/apache/superset/pull/17569) chore: column_type_mappings (@dungdm93) - [#18003](https://github.com/apache/superset/pull/18003) chore(DatasourceEditor): Create Datasource Legacy Editor Feature Flag (@AAfghahi) - [#17996](https://github.com/apache/superset/pull/17996) refactor: remove unused ts-jest (@zhaoyongjie) -- [#17893](https://github.com/apache/superset/pull/17893) refactor: examples data loading for tests (@ofekisr) +- [#17893](https://github.com/apache/superset/pull/17893) refactor: examples data loading for tests (@ofekisr) - [#17967](https://github.com/apache/superset/pull/17967) chore: use new FAB functionality for component schemas update OAS (@dpgaspar) - [#17979](https://github.com/apache/superset/pull/17979) chore: reenable lint for test files (@zhaoyongjie) - [#17965](https://github.com/apache/superset/pull/17965) refactor: move superset-ui dependencies to peerDependencies (@zhaoyongjie) @@ -763,7 +783,7 @@ under the License. - [#17326](https://github.com/apache/superset/pull/17326) chore: migrate DragHandle component from jsx to tsx (@Damans227) - [#17521](https://github.com/apache/superset/pull/17521) chore: bump major on Pillow, optional dependency (@dpgaspar) - [#17452](https://github.com/apache/superset/pull/17452) chore(deps-dev): bump @types/jquery from 3.3.32 to 3.5.8 in /superset-frontend (@dependabot[bot]) -- [#14783](https://github.com/apache/superset/pull/14783) chore: Slovak translation, enabling language and adding first translations (@minho95) +- [#14783](https://github.com/apache/superset/pull/14783) chore: Slovak translation, enabling language and adding first translations (@minho95) - [#17453](https://github.com/apache/superset/pull/17453) chore(deps-dev): bump eslint-import-resolver-webpack from 0.13.1 to 0.13.2 in /superset-frontend (@dependabot[bot]) - [#17502](https://github.com/apache/superset/pull/17502) docs(exploring-data): Add upload excel notes (@aniaan) - [#17522](https://github.com/apache/superset/pull/17522) chore: remove deprecated selects and deps (@villebro) @@ -783,11 +803,11 @@ under the License. - [#17482](https://github.com/apache/superset/pull/17482) chore(deps): bump cachelib from 0.1.1 to 0.4.1 (@villebro) - [#17464](https://github.com/apache/superset/pull/17464) chore(sql_lab): Added Unit Test for stop query exception (@AAfghahi) - [#17454](https://github.com/apache/superset/pull/17454) chore(deps-dev): bump @types/react-loadable from 5.5.4 to 5.5.6 in /superset-frontend (@dependabot[bot]) -- [#17479](https://github.com/apache/superset/pull/17479) refactor(QueryObject): decouple from superset (@ofekisr) +- [#17479](https://github.com/apache/superset/pull/17479) refactor(QueryObject): decouple from superset (@ofekisr) - [#17466](https://github.com/apache/superset/pull/17466) refactor(QueryObject): add QueryObjectFactory to meet SRP (@ofekisr) - [#17465](https://github.com/apache/superset/pull/17465) refactor(QueryObject): decouple from queryContext and clean code (@ofekisr) - [#16868](https://github.com/apache/superset/pull/16868) chore(fr-translation): update whole French translation (based on 1.3.0 release) (@audour) -- [#17461](https://github.com/apache/superset/pull/17461) refactor(ChartDataCommand): remove create queryContext command's responsibly (@ofekisr) +- [#17461](https://github.com/apache/superset/pull/17461) refactor(ChartDataCommand): remove create queryContext command's responsibly (@ofekisr) - [#17427](https://github.com/apache/superset/pull/17427) refactor(monorepo): stage 1 (@zhaoyongjie) - [#17451](https://github.com/apache/superset/pull/17451) chore: Ignore docs directory for dependabot (@hughhhh) - [#17398](https://github.com/apache/superset/pull/17398) test: add native filter default value e2e (@jinghua-qa) @@ -796,7 +816,7 @@ under the License. - [#14576](https://github.com/apache/superset/pull/14576) chore(deps): bump hosted-git-info from 2.8.8 to 2.8.9 in /docs (@dependabot[bot]) - [#17438](https://github.com/apache/superset/pull/17438) chore: bump superset-ui 0.18.25 (@zhaoyongjie) - [#17425](https://github.com/apache/superset/pull/17425) refactor(ChartDataCommand): into two separate commands (@ofekisr) -- [#17407](https://github.com/apache/superset/pull/17407) refactor(TestChartApi): move chart data api tests into TestChartDataApi (@ofekisr) +- [#17407](https://github.com/apache/superset/pull/17407) refactor(TestChartApi): move chart data api tests into TestChartDataApi (@ofekisr) - [#17405](https://github.com/apache/superset/pull/17405) refactor(ChartDataCommand): separate loading query_context form cache into different module (@ofekisr) - [#17403](https://github.com/apache/superset/pull/17403) chore: add dependencies for monorepo (@zhaoyongjie) - [#17400](https://github.com/apache/superset/pull/17400) refactor(ChartData): move chart_data_apis from ChartRestApi ChartDataRestApi (@ofekisr) @@ -856,27 +876,32 @@ under the License. - [#16689](https://github.com/apache/superset/pull/16689) chore: refactor header menu to show in header grid component (@pkdotson) ### 1.4.2 (Sat Mar 19 00:08:06 2022 +0200) + **Features** + - [#19248](https://github.com/apache/superset/pull/19248) feat: add support for comments in adhoc clauses (@villebro) - [#18214](https://github.com/apache/superset/pull/18214) feat(docker-compose): add TAG option (@villebro) **Fixes** + - [#17641](https://github.com/apache/superset/pull/17641) fix(sqla): make text clause escaping optional (@villebro) - [#18566](https://github.com/apache/superset/pull/18566) fix(plugin-chart-echarts): area chart opacity bug (@villebro) ### 1.4.1 + **Database Migrations** **Features** **Fixes** + - [#17980](https://github.com/apache/superset/pull/17980) fix: css template API response, less data (@dpgaspar) - [#17984](https://github.com/apache/superset/pull/17984) fix: Change default SECRET_KEY, improve docs and banner warning on de… (@dpgaspar) - [#17981](https://github.com/apache/superset/pull/17981) fix: API logger output (@dpgaspar) - [#18006](https://github.com/apache/superset/pull/18006) fix: SQL Lab sorting of non-numbers (@etr2460) - [#17573](https://github.com/apache/superset/pull/17573) fix(sqllab): Floating numbers not sorting correctly in result column (@lyndsiWilliams) - [#17961](https://github.com/apache/superset/pull/17961) fix: update slug name (@pkdotson) -- [#17992](https://github.com/apache/superset/pull/17992) fix: dashboard reload crash (@pkdotson) +- [#17992](https://github.com/apache/superset/pull/17992) fix: dashboard reload crash (@pkdotson) - [#18048](https://github.com/apache/superset/pull/18048) fix(dashboard): scope status of native filter not update (@stephenLYZ) - [#16869](https://github.com/apache/superset/pull/16869) fix: handle TIME column serialization (@frafra) From eab0009101a295acf4d8d31df8a57f8fe0deb517 Mon Sep 17 00:00:00 2001 From: Stephen Liu <750188453@qq.com> Date: Thu, 9 Jun 2022 00:59:10 +0800 Subject: [PATCH 05/71] feat(plugin-chart-echarts): [feature-parity] support extra control for the area chart V2 (#16493) * feat(echarts): [feature-parity] support extra control * add extra control for plugin * refactor: extract ExtraControl * fix: lint * fix some problems --- .../components/RadioButtonControl.tsx | 2 +- .../src/MixedTimeseries/transformProps.ts | 4 +- .../src/MixedTimeseries/types.ts | 5 +- .../src/Timeseries/Area/controlPanel.tsx | 36 +++++- .../src/Timeseries/EchartsTimeseries.tsx | 26 ++-- .../src/Timeseries/transformProps.ts | 66 +++++------ .../src/Timeseries/transformers.ts | 13 +- .../src/Timeseries/types.ts | 4 +- .../src/components/ExtraControls.tsx | 112 ++++++++++++++++++ .../plugin-chart-echarts/src/constants.ts | 18 ++- .../plugin-chart-echarts/src/controls.tsx | 6 +- .../plugins/plugin-chart-echarts/src/types.ts | 5 + .../plugin-chart-echarts/src/utils/series.ts | 83 +++++++++++-- .../src/components/Chart/ChartRenderer.jsx | 1 + .../components/gridComponents/Chart.jsx | 3 + .../components/gridComponents/ChartHolder.jsx | 15 ++- .../src/dashboard/containers/Chart.jsx | 4 +- .../charts/getFormDataWithExtraFilters.ts | 8 +- .../util/getFormDataWithExtraFilters.test.ts | 8 ++ 19 files changed, 349 insertions(+), 70 deletions(-) create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/components/ExtraControls.tsx diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/RadioButtonControl.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/RadioButtonControl.tsx index 497e331133470..285b92e66e166 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/RadioButtonControl.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/components/RadioButtonControl.tsx @@ -31,7 +31,7 @@ export interface RadioButtonControlProps { description?: string; options: RadioButtonOption[]; hovered?: boolean; - value?: string; + value?: JsonValue; onChange: (opt: RadioButtonOption[0]) => void; } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts index 139dcd9af70ce..62ed57268f699 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts @@ -190,7 +190,7 @@ export default function transformProps( areaOpacity: opacity, seriesType, showValue, - stack, + stack: Boolean(stack), yAxisIndex, filterState, seriesKey: entry.name, @@ -207,7 +207,7 @@ export default function transformProps( areaOpacity: opacityB, seriesType: seriesTypeB, showValue: showValueB, - stack: stackB, + stack: Boolean(stackB), yAxisIndex: yAxisIndexB, filterState, seriesKey: primarySeries.has(entry.name as string) diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts index b5f37551e1d7c..51938436fbb0e 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts @@ -32,6 +32,7 @@ import { EchartsLegendFormData, EchartsTitleFormData, DEFAULT_TITLE_FORM_DATA, + StackType, } from '../types'; import { DEFAULT_FORM_DATA as TIMESERIES_DEFAULTS, @@ -78,8 +79,8 @@ export type EchartsMixedTimeseriesFormData = QueryFormData & { seriesTypeB: EchartsTimeseriesSeriesType; showValue: boolean; showValueB: boolean; - stack: boolean; - stackB: boolean; + stack: StackType; + stackB: StackType; yAxisIndex?: number; yAxisIndexB?: number; groupby: QueryFormColumn[]; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx index b973cb6782c03..e43dda890386b 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx @@ -34,10 +34,12 @@ import { } from '../types'; import { legendSection, + onlyTotalControl, + showValueControl, richTooltipSection, - showValueSection, xAxisControl, } from '../../controls'; +import { AreaChartExtraControlsOptions } from '../../constants'; const { contributionMode, @@ -132,7 +134,37 @@ const config: ControlPanelConfig = { }, }, ], - ...showValueSection, + [showValueControl], + [ + { + name: 'stack', + config: { + type: 'SelectControl', + label: t('Stacked Style'), + renderTrigger: true, + choices: AreaChartExtraControlsOptions, + default: null, + description: t('Stack series on top of each other'), + }, + }, + ], + [onlyTotalControl], + [ + { + name: 'show_extra_controls', + config: { + type: 'CheckboxControl', + label: t('Extra Controls'), + renderTrigger: true, + default: false, + description: t( + 'Whether to show extra controls or not. Extra controls ' + + 'include things like making mulitBar charts stacked ' + + 'or side by side.', + ), + }, + }, + ], [ { name: 'markerEnabled', diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx index 2bf103e2bd9e4..a9947e0d5520e 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/EchartsTimeseries.tsx @@ -24,8 +24,10 @@ import { EchartsHandler, EventHandlers } from '../types'; import Echart from '../components/Echart'; import { TimeseriesChartTransformedProps } from './types'; import { currentSeries } from '../utils/series'; +import { ExtraControls } from '../components/ExtraControls'; const TIMER_DURATION = 300; + // @ts-ignore export default function EchartsTimeseries({ formData, @@ -36,6 +38,7 @@ export default function EchartsTimeseries({ labelMap, selectedValues, setDataMask, + setControlValue, legendData = [], }: TimeseriesChartTransformedProps) { const { emitFilter, stack } = formData; @@ -120,7 +123,7 @@ export default function EchartsTimeseries({ }, }); }, - [groupby, labelMap, setDataMask], + [groupby, labelMap, setDataMask, emitFilter], ); const eventHandlers: EventHandlers = { @@ -195,14 +198,17 @@ export default function EchartsTimeseries({ }; return ( - + <> + + + ); } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts index db142c8aa2875..89d5c1e03b31d 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts @@ -30,6 +30,7 @@ import { isIntervalAnnotationLayer, isTimeseriesAnnotationLayer, TimeseriesChartDataResponseResult, + t, } from '@superset-ui/core'; import { isDerivedSeries } from '@superset-ui/chart-controls'; import { EChartsCoreOption, SeriesOption } from 'echarts'; @@ -51,6 +52,8 @@ import { getAxisType, getColtypesMapping, getLegendProps, + extractDataTotalValues, + extractShowValueIndexes, } from '../utils/series'; import { extractAnnotationLabels } from '../utils/annotation'; import { @@ -72,7 +75,11 @@ import { transformSeries, transformTimeseriesAnnotation, } from './transformers'; -import { TIMESERIES_CONSTANTS, TIMEGRAIN_TO_TIMESTAMP } from '../constants'; +import { + AreaChartExtraControlsValue, + TIMESERIES_CONSTANTS, + TIMEGRAIN_TO_TIMESTAMP, +} from '../constants'; export default function transformProps( chartProps: EchartsTimeseriesChartProps, @@ -140,46 +147,35 @@ export default function transformProps( const xAxisCol = verboseMap[xAxisOrig] || getColumnLabel(xAxisOrig || DTTM_ALIAS); const isHorizontal = orientation === OrientationType.horizontal; + const { totalStackedValues, thresholdValues } = extractDataTotalValues( + rebasedData, + { + stack, + percentageThreshold, + xAxisCol, + }, + ); const rawSeries = extractSeries(rebasedData, { fillNeighborValue: stack && !forecastEnabled ? 0 : undefined, xAxis: xAxisCol, removeNulls: seriesType === EchartsTimeseriesSeriesType.Scatter, + stack, + totalStackedValues, isHorizontal, }); + const showValueIndexes = extractShowValueIndexes(rawSeries, { + stack, + }); const seriesContexts = extractForecastSeriesContexts( Object.values(rawSeries).map(series => series.name as string), ); + const isAreaExpand = stack === AreaChartExtraControlsValue.Expand; const xAxisDataType = dataTypes?.[xAxisCol]; const xAxisType = getAxisType(xAxisDataType); const series: SeriesOption[] = []; - const formatter = getNumberFormatter(contributionMode ? ',.0%' : yAxisFormat); - - const totalStackedValues: number[] = []; - const showValueIndexes: number[] = []; - const thresholdValues: number[] = []; - - rebasedData.forEach(data => { - const values = Object.keys(data).reduce((prev, curr) => { - if (curr === xAxisCol) { - return prev; - } - const value = data[curr] || 0; - return prev + (value as number); - }, 0); - totalStackedValues.push(values); - thresholdValues.push(((percentageThreshold || 0) / 100) * values); - }); - - if (stack) { - rawSeries.forEach((entry, seriesIndex) => { - const { data = [] } = entry; - (data as [Date, number][]).forEach((datum, dataIndex) => { - if (datum[1] !== null) { - showValueIndexes[dataIndex] = seriesIndex; - } - }); - }); - } + const formatter = getNumberFormatter( + contributionMode || isAreaExpand ? ',.0%' : yAxisFormat, + ); rawSeries.forEach(entry => { const lineStyle = isDerivedSeries(entry, chartProps.rawFormData) @@ -266,7 +262,7 @@ export default function transformProps( let [min, max] = (yAxisBounds || []).map(parseYAxisBound); // default to 0-100% range when doing row-level contribution chart - if (contributionMode === 'row' && stack) { + if ((contributionMode === 'row' || isAreaExpand) && stack) { if (min === undefined) min = 0; if (max === undefined) max = 1; } @@ -291,7 +287,10 @@ export default function transformProps( {}, ); - const { setDataMask = () => {} } = hooks; + const { + setDataMask = () => {}, + setControlValue = (...args: unknown[]) => {}, + } = hooks; const addYAxisLabelOffset = !!yAxisTitle; const addXAxisLabelOffset = !!xAxisTitle; @@ -406,8 +405,8 @@ export default function transformProps( dataZoom: { yAxisIndex: false, title: { - zoom: 'zoom area', - back: 'restore zoom', + zoom: t('zoom area'), + back: t('restore zoom'), }, }, }, @@ -433,6 +432,7 @@ export default function transformProps( labelMap, selectedValues, setDataMask, + setControlValue, width, legendData, }; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts index 4ab7309dbca8c..93565de46c278 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts @@ -52,7 +52,7 @@ import { import { MarkLine1DDataItemOption } from 'echarts/types/src/component/marker/MarkLineModel'; import { extractForecastSeriesContext } from '../utils/forecast'; -import { ForecastSeriesEnum, LegendOrientation } from '../types'; +import { ForecastSeriesEnum, LegendOrientation, StackType } from '../types'; import { EchartsTimeseriesSeriesType } from './types'; import { @@ -62,7 +62,11 @@ import { parseAnnotationOpacity, } from '../utils/annotation'; import { currentSeries, getChartPadding } from '../utils/series'; -import { OpacityEnum, TIMESERIES_CONSTANTS } from '../constants'; +import { + AreaChartExtraControlsValue, + OpacityEnum, + TIMESERIES_CONSTANTS, +} from '../constants'; export function transformSeries( series: SeriesOption, @@ -75,7 +79,7 @@ export function transformSeries( markerSize?: number; areaOpacity?: number; seriesType?: EchartsTimeseriesSeriesType; - stack?: boolean; + stack?: StackType; yAxisIndex?: number; showValue?: boolean; onlyTotal?: boolean; @@ -225,6 +229,7 @@ export function transformSeries( const { value, dataIndex, seriesIndex, seriesName } = params; const numericValue = isHorizontal ? value[0] : value[1]; const isSelectedLegend = currentSeries.legend === seriesName; + const isAreaExpand = stack === AreaChartExtraControlsValue.Expand; if (!formatter) return numericValue; if (!stack || isSelectedLegend) return formatter(numericValue); if (!onlyTotal) { @@ -234,7 +239,7 @@ export function transformSeries( return ''; } if (seriesIndex === showValueIndexes[dataIndex]) { - return formatter(totalStackedValues[dataIndex]); + return formatter(isAreaExpand ? 1 : totalStackedValues[dataIndex]); } return ''; }, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts index 0d2499ccfcf6d..d9b7708146df3 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts @@ -31,6 +31,7 @@ import { EChartTransformedProps, EchartsTitleFormData, DEFAULT_TITLE_FORM_DATA, + StackType, } from '../types'; export enum EchartsTimeseriesContributionType { @@ -72,7 +73,7 @@ export type EchartsTimeseriesFormData = QueryFormData & { orderDesc: boolean; rowLimit: number; seriesType: EchartsTimeseriesSeriesType; - stack: boolean; + stack: StackType; tooltipTimeFormat?: string; truncateYAxis: boolean; yAxisFormat?: string; @@ -86,6 +87,7 @@ export type EchartsTimeseriesFormData = QueryFormData & { groupby: QueryFormColumn[]; showValue: boolean; onlyTotal: boolean; + showExtraControls: boolean; percentageThreshold: number; orientation?: OrientationType; } & EchartsLegendFormData & diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/components/ExtraControls.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/components/ExtraControls.tsx new file mode 100644 index 0000000000000..10217b3add730 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/components/ExtraControls.tsx @@ -0,0 +1,112 @@ +/** + * 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, { useState, useEffect, useMemo, useCallback } from 'react'; +import { HandlerFunction, JsonValue, styled } from '@superset-ui/core'; +import { + RadioButtonOption, + sharedControlComponents, +} from '@superset-ui/chart-controls'; +import { AreaChartExtraControlsOptions } from '../constants'; + +const { RadioButtonControl } = sharedControlComponents; + +const ExtraControlsWrapper = styled.div` + text-align: center; +`; + +export function useExtraControl< + F extends { + stack: any; + area: boolean; + }, +>({ + formData, + setControlValue, +}: { + formData: F; + setControlValue?: HandlerFunction; +}) { + const { stack, area } = formData; + const [extraValue, setExtraValue] = useState( + stack ?? undefined, + ); + + useEffect(() => { + setExtraValue(stack); + }, [stack]); + + const extraControlsOptions = useMemo(() => { + if (area) { + return AreaChartExtraControlsOptions; + } + return []; + }, [area]); + + const extraControlsHandler = useCallback( + (value: RadioButtonOption[0]) => { + if (area) { + if (setControlValue) { + setControlValue('stack', value); + setExtraValue(value); + } + } + }, + [area, setControlValue], + ); + + return { + extraControlsOptions, + extraControlsHandler, + extraValue, + }; +} + +export function ExtraControls< + F extends { + stack: any; + area: boolean; + showExtraControls: boolean; + }, +>({ + formData, + setControlValue, +}: { + formData: F; + setControlValue?: HandlerFunction; +}) { + const { extraControlsOptions, extraControlsHandler, extraValue } = + useExtraControl({ + formData, + setControlValue, + }); + + if (!formData.showExtraControls) { + return null; + } + + return ( + + + + ); +} diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts b/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts index deef2f2e8c6f5..513a0bebc1843 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts @@ -17,7 +17,8 @@ * under the License. */ -import { TimeGranularity } from '@superset-ui/core'; +import { JsonValue, t, TimeGranularity } from '@superset-ui/core'; +import { ReactNode } from 'react'; import { LabelPositionEnum } from './types'; // eslint-disable-next-line import/prefer-default-export @@ -37,6 +38,7 @@ export const TIMESERIES_CONSTANTS = { dataZoomStart: 0, dataZoomEnd: 100, yAxisLabelTopOffset: 20, + extraControlsOffset: 22, }; export const LABEL_POSITION: [LabelPositionEnum, string][] = [ @@ -61,6 +63,20 @@ export enum OpacityEnum { NonTransparent = 1, } +export enum AreaChartExtraControlsValue { + Stack = 'Stack', + Expand = 'Expand', +} + +export const AreaChartExtraControlsOptions: [ + JsonValue, + Exclude, +][] = [ + [null, t('None')], + [AreaChartExtraControlsValue.Stack, t('Stack')], + [AreaChartExtraControlsValue.Expand, t('Expand')], +]; + export const TIMEGRAIN_TO_TIMESTAMP = { [TimeGranularity.HOUR]: 3600 * 1000, [TimeGranularity.DAY]: 3600 * 1000 * 24, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx index eca472388774f..b8d54fc09a41a 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx @@ -108,7 +108,7 @@ export const legendSection: ControlSetRow[] = [ [legendMarginControl], ]; -const showValueControl: ControlSetItem = { +export const showValueControl: ControlSetItem = { name: 'show_value', config: { type: 'CheckboxControl', @@ -119,7 +119,7 @@ const showValueControl: ControlSetItem = { }, }; -const stackControl: ControlSetItem = { +export const stackControl: ControlSetItem = { name: 'stack', config: { type: 'CheckboxControl', @@ -130,7 +130,7 @@ const stackControl: ControlSetItem = { }, }; -const onlyTotalControl: ControlSetItem = { +export const onlyTotalControl: ControlSetItem = { name: 'only_total', config: { type: 'CheckboxControl', diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/types.ts index f50397c9ef518..d84b7079c4794 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/types.ts @@ -18,12 +18,14 @@ */ import { DataRecordValue, + HandlerFunction, QueryFormColumn, SetDataMaskHook, } from '@superset-ui/core'; import { EChartsCoreOption, ECharts } from 'echarts'; import { TooltipMarker } from 'echarts/types/src/util/format'; import { OptionName } from 'echarts/types/src/util/types'; +import { AreaChartExtraControlsValue } from './constants'; export type EchartsStylesProps = { height: number; @@ -115,6 +117,7 @@ export interface EChartTransformedProps { echartOptions: EChartsCoreOption; emitFilter: boolean; setDataMask: SetDataMaskHook; + setControlValue?: HandlerFunction; labelMap: Record; groupby: QueryFormColumn[]; selectedValues: Record; @@ -137,4 +140,6 @@ export const DEFAULT_TITLE_FORM_DATA: EchartsTitleFormData = { yAxisTitlePosition: 'Top', }; +export type StackType = boolean | null | Partial; + export * from './Timeseries/types'; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts index fa8a23138cfd8..23710cd6d1944 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts @@ -28,20 +28,79 @@ import { TimeFormatter, } from '@superset-ui/core'; import { format, LegendComponentOption, SeriesOption } from 'echarts'; -import { NULL_STRING, TIMESERIES_CONSTANTS } from '../constants'; -import { LegendOrientation, LegendType } from '../types'; +import { + AreaChartExtraControlsValue, + NULL_STRING, + TIMESERIES_CONSTANTS, +} from '../constants'; +import { LegendOrientation, LegendType, StackType } from '../types'; import { defaultLegendPadding } from '../defaults'; function isDefined(value: T | undefined | null): boolean { return value !== undefined && value !== null; } +export function extractDataTotalValues( + data: DataRecord[], + opts: { + stack: StackType; + percentageThreshold: number; + xAxisCol: string; + }, +): { + totalStackedValues: number[]; + thresholdValues: number[]; +} { + const totalStackedValues: number[] = []; + const thresholdValues: number[] = []; + const { stack, percentageThreshold, xAxisCol } = opts; + if (stack) { + data.forEach(datum => { + const values = Object.keys(datum).reduce((prev, curr) => { + if (curr === xAxisCol) { + return prev; + } + const value = datum[curr] || 0; + return prev + (value as number); + }, 0); + totalStackedValues.push(values); + thresholdValues.push(((percentageThreshold || 0) / 100) * values); + }); + } + return { + totalStackedValues, + thresholdValues, + }; +} + +export function extractShowValueIndexes( + series: SeriesOption[], + opts: { + stack: StackType; + }, +): number[] { + const showValueIndexes: number[] = []; + if (opts.stack) { + series.forEach((entry, seriesIndex) => { + const { data = [] } = entry; + (data as [any, number][]).forEach((datum, dataIndex) => { + if (datum[1] !== null) { + showValueIndexes[dataIndex] = seriesIndex; + } + }); + }); + } + return showValueIndexes; +} + export function extractSeries( data: DataRecord[], opts: { fillNeighborValue?: number; xAxis?: string; removeNulls?: boolean; + stack?: StackType; + totalStackedValues?: number[]; isHorizontal?: boolean; } = {}, ): SeriesOption[] { @@ -49,6 +108,8 @@ export function extractSeries( fillNeighborValue, xAxis = DTTM_ALIAS, removeNulls = false, + stack = false, + totalStackedValues = [], isHorizontal = false, } = opts; if (data.length === 0) return []; @@ -66,14 +127,20 @@ export function extractSeries( .map((row, idx) => { const isNextToDefinedValue = isDefined(rows[idx - 1]?.[key]) || isDefined(rows[idx + 1]?.[key]); - return [ - row[xAxis], + const isFillNeighborValue = !isDefined(row[key]) && isNextToDefinedValue && - fillNeighborValue !== undefined - ? fillNeighborValue - : row[key], - ]; + fillNeighborValue !== undefined; + let value: DataRecordValue | undefined = row[key]; + if (isFillNeighborValue) { + value = fillNeighborValue; + } else if ( + stack === AreaChartExtraControlsValue.Expand && + totalStackedValues.length > 0 + ) { + value = ((value || 0) as number) / totalStackedValues[idx]; + } + return [row[xAxis], value]; }) .filter(obs => !removeNulls || (obs[0] !== null && obs[1] !== null)) .map(obs => (isHorizontal ? [obs[1], obs[0]] : obs)), diff --git a/superset-frontend/src/components/Chart/ChartRenderer.jsx b/superset-frontend/src/components/Chart/ChartRenderer.jsx index 45feb6ffd57ee..ed330ab7afc95 100644 --- a/superset-frontend/src/components/Chart/ChartRenderer.jsx +++ b/superset-frontend/src/components/Chart/ChartRenderer.jsx @@ -113,6 +113,7 @@ class ChartRenderer extends React.Component { nextProps.labelColors !== this.props.labelColors || nextProps.sharedLabelColors !== this.props.sharedLabelColors || nextProps.formData.color_scheme !== this.props.formData.color_scheme || + nextProps.formData.stack !== this.props.formData.stack || nextProps.cacheBusterProp !== this.props.cacheBusterProp ); } diff --git a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx index e5d19e931c58d..5232f51e4d597 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx @@ -51,6 +51,7 @@ const propTypes = { updateSliceName: PropTypes.func.isRequired, isComponentVisible: PropTypes.bool, handleToggleFullSize: PropTypes.func.isRequired, + setControlValue: PropTypes.func, // from redux chart: chartPropShape.isRequired, @@ -348,6 +349,7 @@ export default class Chart extends React.Component { filterState, handleToggleFullSize, isFullSize, + setControlValue, filterboxMigrationState, postTransformProps, datasetsStatus, @@ -475,6 +477,7 @@ export default class Chart extends React.Component { timeout={timeout} triggerQuery={chart.triggerQuery} vizType={slice.viz_type} + setControlValue={setControlValue} isDeactivatedViz={isDeactivatedViz} filterboxMigrationState={filterboxMigrationState} postTransformProps={postTransformProps} diff --git a/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx b/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx index eeac8566159ac..2363b1610e4fa 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx @@ -191,12 +191,14 @@ class ChartHolder extends React.Component { outlinedComponentId: null, outlinedColumnName: null, directPathLastUpdated: 0, + extraControls: {}, }; this.handleChangeFocus = this.handleChangeFocus.bind(this); this.handleDeleteComponent = this.handleDeleteComponent.bind(this); this.handleUpdateSliceName = this.handleUpdateSliceName.bind(this); this.handleToggleFullSize = this.handleToggleFullSize.bind(this); + this.handleExtraControl = this.handleExtraControl.bind(this); this.handlePostTransformProps = this.handlePostTransformProps.bind(this); } @@ -252,13 +254,22 @@ class ChartHolder extends React.Component { setFullSizeChartId(isFullSize ? null : chartId); } + handleExtraControl(name, value) { + this.setState(prevState => ({ + extraControls: { + ...prevState.extraControls, + [name]: value, + }, + })); + } + handlePostTransformProps(props) { this.props.postAddSliceFromDashboard(); return props; } render() { - const { isFocused } = this.state; + const { isFocused, extraControls } = this.state; const { component, parentComponent, @@ -374,6 +385,8 @@ class ChartHolder extends React.Component { isComponentVisible={isComponentVisible} handleToggleFullSize={this.handleToggleFullSize} isFullSize={isFullSize} + setControlValue={this.handleExtraControl} + extraControls={extraControls} postTransformProps={this.handlePostTransformProps} /> {editMode && ( diff --git a/superset-frontend/src/dashboard/containers/Chart.jsx b/superset-frontend/src/dashboard/containers/Chart.jsx index 79b4e936da7a6..81d06b8566239 100644 --- a/superset-frontend/src/dashboard/containers/Chart.jsx +++ b/superset-frontend/src/dashboard/containers/Chart.jsx @@ -55,7 +55,7 @@ function mapStateToProps( }, ownProps, ) { - const { id } = ownProps; + const { id, extraControls, setControlValue } = ownProps; const chart = chartQueries[id] || EMPTY_OBJECT; const datasource = (chart && chart.form_data && datasources[chart.form_data.datasource]) || @@ -76,6 +76,7 @@ function mapStateToProps( sliceId: id, nativeFilters, dataMask, + extraControls, labelColors, sharedLabelColors, }); @@ -100,6 +101,7 @@ function mapStateToProps( ownState: dataMask[id]?.ownState, filterState: dataMask[id]?.filterState, maxRows: common.conf.SQL_MAX_ROW, + setControlValue, filterboxMigrationState: dashboardState.filterboxMigrationState, datasetsStatus, }; diff --git a/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts b/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts index 54e0417b27718..0bbabfcde52d1 100644 --- a/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts +++ b/superset-frontend/src/dashboard/util/charts/getFormDataWithExtraFilters.ts @@ -45,6 +45,7 @@ export interface GetFormDataWithExtraFiltersArguments { sliceId: number; dataMask: DataMaskStateWithId; nativeFilters: NativeFiltersState; + extraControls: Record; labelColors?: Record; sharedLabelColors?: Record; } @@ -63,6 +64,7 @@ export default function getFormDataWithExtraFilters({ sliceId, layout, dataMask, + extraControls, labelColors, sharedLabelColors, }: GetFormDataWithExtraFiltersArguments) { @@ -85,6 +87,9 @@ export default function getFormDataWithExtraFilters({ !!cachedFormData && areObjectsEqual(cachedFormData?.dataMask, dataMask, { ignoreUndefined: true, + }) && + areObjectsEqual(cachedFormData?.extraControls, extraControls, { + ignoreUndefined: true, }) ) { return cachedFormData; @@ -117,10 +122,11 @@ export default function getFormDataWithExtraFilters({ ...(colorScheme && { color_scheme: colorScheme }), extra_filters: getEffectiveExtraFilters(filters), ...extraData, + ...extraControls, }; cachedFiltersByChart[sliceId] = filters; - cachedFormdataByChart[sliceId] = { ...formData, dataMask }; + cachedFormdataByChart[sliceId] = { ...formData, dataMask, extraControls }; return formData; } diff --git a/superset-frontend/src/dashboard/util/getFormDataWithExtraFilters.test.ts b/superset-frontend/src/dashboard/util/getFormDataWithExtraFilters.test.ts index fda5edc1a92e4..021a488e37794 100644 --- a/superset-frontend/src/dashboard/util/getFormDataWithExtraFilters.test.ts +++ b/superset-frontend/src/dashboard/util/getFormDataWithExtraFilters.test.ts @@ -71,6 +71,9 @@ describe('getFormDataWithExtraFilters', () => { }, }, layout: {}, + extraControls: { + stack: 'Stacked', + }, }; it('should include filters from the passed filters', () => { @@ -87,4 +90,9 @@ describe('getFormDataWithExtraFilters', () => { val: ['pink', 'purple'], }); }); + + it('should compose extra control', () => { + const result = getFormDataWithExtraFilters(mockArgs); + expect(result.stack).toEqual('Stacked'); + }); }); From 5bfc95e79e89961967ba4acc8d24131157ccd16b Mon Sep 17 00:00:00 2001 From: Diego Medina Date: Wed, 8 Jun 2022 18:52:53 -0400 Subject: [PATCH 06/71] feat: When editing the label/title in the Metrics popover, hitting Enter should save what you've typed (#19898) * feature: When editing the label/title in the Metrics popover, hitting Enter should save what you've typed * Apply emotion templating to input/input labels --- .../src/assets/stylesheets/superset.less | 6 - .../DndColumnSelectPopoverTitle.jsx | 11 +- .../AdhocMetricEditPopoverTitle.jsx | 115 -------------- .../AdhocMetricEditPopoverTitle.test.jsx | 70 --------- .../AdhocMetricEditPopoverTitle.test.tsx | 141 ++++++++++++++++++ .../AdhocMetricEditPopoverTitle.tsx | 127 ++++++++++++++++ 6 files changed, 276 insertions(+), 194 deletions(-) delete mode 100644 superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopoverTitle.jsx delete mode 100644 superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopoverTitle.test.jsx create mode 100644 superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopoverTitle.test.tsx create mode 100644 superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopoverTitle.tsx diff --git a/superset-frontend/src/assets/stylesheets/superset.less b/superset-frontend/src/assets/stylesheets/superset.less index 0cf419b30d190..5808d0144bc73 100644 --- a/superset-frontend/src/assets/stylesheets/superset.less +++ b/superset-frontend/src/assets/stylesheets/superset.less @@ -518,12 +518,6 @@ tr.reactable-column-header th.reactable-header-sortable { padding-right: 17px; } -.metric-edit-popover-label-input { - border-radius: @border-radius-large; - height: 30px; - padding-left: 10px; -} - .align-right { text-align: right; } diff --git a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelectPopoverTitle.jsx b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelectPopoverTitle.jsx index eecce0b33335b..b50abb9aae6ee 100644 --- a/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelectPopoverTitle.jsx +++ b/superset-frontend/src/explore/components/controls/DndColumnSelectControl/DndColumnSelectPopoverTitle.jsx @@ -17,10 +17,16 @@ * under the License. */ import React, { useCallback, useState } from 'react'; -import { t } from '@superset-ui/core'; +import { t, styled } from '@superset-ui/core'; import { Input } from 'src/components/Input'; import { Tooltip } from 'src/components/Tooltip'; +const StyledInput = styled(Input)` + border-radius: ${({ theme }) => theme.borderRadius}; + height: 26px; + padding-left: ${({ theme }) => theme.gridUnit * 2.5}px; +`; + export const DndColumnSelectPopoverTitle = ({ title, onChange, @@ -63,8 +69,7 @@ export const DndColumnSelectPopoverTitle = ({ } return isEditMode ? ( - {title.label || defaultLabel} - ); - } - - return this.state.isEditMode ? ( - - ) : ( - - - {title.label || defaultLabel} -   - - - - ); - } -} -AdhocMetricEditPopoverTitle.propTypes = propTypes; diff --git a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopoverTitle.test.jsx b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopoverTitle.test.jsx deleted file mode 100644 index dd2b007bf91be..0000000000000 --- a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopoverTitle.test.jsx +++ /dev/null @@ -1,70 +0,0 @@ -/** - * 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. - */ -/* eslint-disable no-unused-expressions */ -import React from 'react'; -import sinon from 'sinon'; -import { shallow } from 'enzyme'; -import { Tooltip } from 'src/components/Tooltip'; - -import AdhocMetricEditPopoverTitle from 'src/explore/components/controls/MetricControl/AdhocMetricEditPopoverTitle'; - -const title = { - label: 'Title', - hasCustomLabel: false, -}; - -function setup(overrides) { - const onChange = sinon.spy(); - const props = { - title, - onChange, - ...overrides, - }; - const wrapper = shallow(); - return { wrapper, onChange }; -} - -describe('AdhocMetricEditPopoverTitle', () => { - it('renders an OverlayTrigger wrapper with the title', () => { - const { wrapper } = setup(); - expect(wrapper.find(Tooltip)).toExist(); - expect( - wrapper.find('[data-test="AdhocMetricEditTitle#trigger"]').text(), - ).toBe(`${title.label}\xa0`); - }); - - it('transfers to edit mode when clicked', () => { - const { wrapper } = setup(); - expect(wrapper.state('isEditMode')).toBe(false); - wrapper - .find('[data-test="AdhocMetricEditTitle#trigger"]') - .simulate('click'); - expect(wrapper.state('isEditMode')).toBe(true); - }); - - it('Render non-interactive span with title when edit is disabled', () => { - const { wrapper } = setup({ isEditDisabled: true }); - expect( - wrapper.find('[data-test="AdhocMetricTitle"]').exists(), - ).toBeTruthy(); - expect( - wrapper.find('[data-test="AdhocMetricEditTitle#trigger"]').exists(), - ).toBeFalsy(); - }); -}); diff --git a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopoverTitle.test.tsx b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopoverTitle.test.tsx new file mode 100644 index 0000000000000..a91e0cac6f5af --- /dev/null +++ b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopoverTitle.test.tsx @@ -0,0 +1,141 @@ +/** + * 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 userEvent from '@testing-library/user-event'; +import { + screen, + render, + fireEvent, + waitFor, +} from 'spec/helpers/testing-library'; + +import AdhocMetricEditPopoverTitle, { + AdhocMetricEditPopoverTitleProps, +} from 'src/explore/components/controls/MetricControl/AdhocMetricEditPopoverTitle'; + +const titleProps = { + label: 'COUNT(*)', + hasCustomLabel: false, +}; + +const setup = (props: Partial = {}) => { + const onChange = jest.fn(); + + const { container } = render( + , + ); + + return { container, onChange }; +}; + +test('should render', async () => { + const { container } = setup(); + expect(container).toBeInTheDocument(); + + expect(screen.queryByTestId('AdhocMetricTitle')).not.toBeInTheDocument(); + expect(screen.getByText(titleProps.label)).toBeVisible(); +}); + +test('should render tooltip on hover', async () => { + const { container } = setup(); + + expect(screen.queryByText('Click to edit label')).not.toBeInTheDocument(); + fireEvent.mouseOver(screen.getByTestId('AdhocMetricEditTitle#trigger')); + + expect(await screen.findByText('Click to edit label')).toBeInTheDocument(); + expect( + container.parentElement?.getElementsByClassName('ant-tooltip-hidden') + .length, + ).toBe(0); + + fireEvent.mouseOut(screen.getByTestId('AdhocMetricEditTitle#trigger')); + await waitFor(() => { + expect( + container.parentElement?.getElementsByClassName('ant-tooltip-hidden') + .length, + ).toBe(1); + }); +}); + +test('render non-interactive span with title when edit is disabled', async () => { + const { container } = setup({ isEditDisabled: true }); + expect(container).toBeInTheDocument(); + + expect(screen.queryByTestId('AdhocMetricTitle')).toBeInTheDocument(); + expect(screen.getByText(titleProps.label)).toBeVisible(); + expect( + screen.queryByTestId('AdhocMetricEditTitle#trigger'), + ).not.toBeInTheDocument(); +}); + +test('render default label if no title is provided', async () => { + const { container } = setup({ title: undefined }); + expect(container).toBeInTheDocument(); + + expect(screen.queryByTestId('AdhocMetricTitle')).not.toBeInTheDocument(); + expect(screen.getByText('My metric')).toBeVisible(); +}); + +test('start and end the title edit mode', async () => { + const { container, onChange } = setup(); + expect(container).toBeInTheDocument(); + + expect(container.getElementsByTagName('i')[0]).toBeVisible(); + expect(screen.getByText(titleProps.label)).toBeVisible(); + expect( + screen.queryByTestId('AdhocMetricEditTitle#input'), + ).not.toBeInTheDocument(); + + fireEvent.click( + container.getElementsByClassName('AdhocMetricEditPopoverTitle')[0], + ); + + expect(await screen.findByTestId('AdhocMetricEditTitle#input')).toBeVisible(); + userEvent.type(screen.getByTestId('AdhocMetricEditTitle#input'), 'Test'); + + expect(onChange).toHaveBeenCalledTimes(4); + fireEvent.keyPress(screen.getByTestId('AdhocMetricEditTitle#input'), { + key: 'Enter', + charCode: 13, + }); + + expect( + screen.queryByTestId('AdhocMetricEditTitle#input'), + ).not.toBeInTheDocument(); + + fireEvent.click( + container.getElementsByClassName('AdhocMetricEditPopoverTitle')[0], + ); + + expect(await screen.findByTestId('AdhocMetricEditTitle#input')).toBeVisible(); + userEvent.type( + screen.getByTestId('AdhocMetricEditTitle#input'), + 'Second test', + ); + expect(onChange).toHaveBeenCalled(); + + fireEvent.blur(screen.getByTestId('AdhocMetricEditTitle#input')); + expect( + screen.queryByTestId('AdhocMetricEditTitle#input'), + ).not.toBeInTheDocument(); +}); diff --git a/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopoverTitle.tsx b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopoverTitle.tsx new file mode 100644 index 0000000000000..da6a2739c3871 --- /dev/null +++ b/superset-frontend/src/explore/components/controls/MetricControl/AdhocMetricEditPopoverTitle.tsx @@ -0,0 +1,127 @@ +/** + * 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, { + ChangeEventHandler, + FocusEvent, + KeyboardEvent, + useCallback, + useState, +} from 'react'; +import { t, styled } from '@superset-ui/core'; +import { Input } from 'src/components/Input'; +import { Tooltip } from 'src/components/Tooltip'; + +const TitleLabel = styled.span` + display: inline-block; + padding: 2px 0; +`; + +const StyledInput = styled(Input)` + border-radius: ${({ theme }) => theme.borderRadius}; + height: 26px; + padding-left: ${({ theme }) => theme.gridUnit * 2.5}px; +`; + +export interface AdhocMetricEditPopoverTitleProps { + title?: { + label?: string; + hasCustomLabel?: boolean; + }; + isEditDisabled?: boolean; + onChange: ChangeEventHandler; +} + +const AdhocMetricEditPopoverTitle: React.FC = + ({ title, isEditDisabled, onChange }) => { + const [isHovered, setIsHovered] = useState(false); + const [isEditMode, setIsEditMode] = useState(false); + + const defaultLabel = t('My metric'); + + const handleMouseOver = useCallback(() => setIsHovered(true), []); + const handleMouseOut = useCallback(() => setIsHovered(false), []); + const handleClick = useCallback(() => setIsEditMode(true), []); + const handleBlur = useCallback(() => setIsEditMode(false), []); + + const handleKeyPress = useCallback( + (ev: KeyboardEvent) => { + if (ev.key === 'Enter') { + ev.preventDefault(); + handleBlur(); + } + }, + [handleBlur], + ); + + const handleInputBlur = useCallback( + (e: FocusEvent) => { + if (e.target.value === '') { + onChange(e); + } + + handleBlur(); + }, + [onChange, handleBlur], + ); + + if (isEditDisabled) { + return ( + {title?.label || defaultLabel} + ); + } + + if (isEditMode) { + return ( + + ); + } + + return ( + + + {title?.label || defaultLabel} +   + + + + ); + }; + +export default AdhocMetricEditPopoverTitle; From 1e5cacda8f939874bc05832234f24579b7400c3a Mon Sep 17 00:00:00 2001 From: smileydev <47900232+prosdev0107@users.noreply.github.com> Date: Wed, 8 Jun 2022 18:00:08 -0500 Subject: [PATCH 07/71] fix(explore): make to fix the issue of explore error broken when see more/less (#20282) --- superset-frontend/src/explore/components/ExploreChartPanel.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/superset-frontend/src/explore/components/ExploreChartPanel.jsx b/superset-frontend/src/explore/components/ExploreChartPanel.jsx index 9fc7caef62803..d52c9eb96c1c0 100644 --- a/superset-frontend/src/explore/components/ExploreChartPanel.jsx +++ b/superset-frontend/src/explore/components/ExploreChartPanel.jsx @@ -220,6 +220,7 @@ const ExploreChartPanel = ({ css={css` min-height: 0; flex: 1; + overflow: auto; `} ref={chartPanelRef} > From fd129873ceeb74dc2e59d9b94ed1c9d006f1386c Mon Sep 17 00:00:00 2001 From: Yongjie Zhao Date: Thu, 9 Jun 2022 09:11:34 +0800 Subject: [PATCH 08/71] feat: multiple results pane on explore and dashboard (#20277) --- .../components/SliceHeaderControls/index.tsx | 5 +- .../components/DataTableControl/index.tsx | 9 +- .../DataTablesPane/DataTablesPane.tsx | 56 ++++-- .../components/ResultsPaneOnDashboard.tsx | 69 ++++++++ .../DataTablesPane/components/SamplesPane.tsx | 3 +- .../components/SingleQueryResultPane.tsx | 73 ++++++++ .../DataTablesPane/components/index.ts | 3 +- .../{ResultsPane.tsx => useResultsPane.tsx} | 119 +++++-------- .../{ => test}/DataTablesPane.test.tsx | 122 ++----------- .../test/ResultsPaneOnDashboard.test.tsx | 160 ++++++++++++++++++ .../DataTablesPane/test/SamplesPane.test.tsx | 106 ++++++++++++ .../DataTablesPane/test/fixture.tsx | 119 +++++++++++++ .../components/DataTablesPane/types.ts | 23 +++ .../components/DataTablesPane/utils.ts | 24 +++ 14 files changed, 678 insertions(+), 213 deletions(-) create mode 100644 superset-frontend/src/explore/components/DataTablesPane/components/ResultsPaneOnDashboard.tsx create mode 100644 superset-frontend/src/explore/components/DataTablesPane/components/SingleQueryResultPane.tsx rename superset-frontend/src/explore/components/DataTablesPane/components/{ResultsPane.tsx => useResultsPane.tsx} (50%) rename superset-frontend/src/explore/components/DataTablesPane/{ => test}/DataTablesPane.test.tsx (64%) create mode 100644 superset-frontend/src/explore/components/DataTablesPane/test/ResultsPaneOnDashboard.test.tsx create mode 100644 superset-frontend/src/explore/components/DataTablesPane/test/SamplesPane.test.tsx create mode 100644 superset-frontend/src/explore/components/DataTablesPane/test/fixture.tsx create mode 100644 superset-frontend/src/explore/components/DataTablesPane/utils.ts diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx index dcec62d88c0e4..6b39e18449354 100644 --- a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx +++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx @@ -36,7 +36,7 @@ import Icons from 'src/components/Icons'; import ModalTrigger from 'src/components/ModalTrigger'; import Button from 'src/components/Button'; import ViewQueryModal from 'src/explore/components/controls/ViewQueryModal'; -import { ResultsPane } from 'src/explore/components/DataTablesPane'; +import { ResultsPaneOnDashboard } from 'src/explore/components/DataTablesPane'; const MENU_KEYS = { CROSS_FILTER_SCOPING: 'cross_filter_scoping', @@ -340,11 +340,12 @@ class SliceHeaderControls extends React.PureComponent< } modalTitle={t('Chart Data: %s', slice.slice_name)} modalBody={ - } modalFooter={ diff --git a/superset-frontend/src/explore/components/DataTableControl/index.tsx b/superset-frontend/src/explore/components/DataTableControl/index.tsx index fb8af865a3914..fdc74d7bb4cde 100644 --- a/superset-frontend/src/explore/components/DataTableControl/index.tsx +++ b/superset-frontend/src/explore/components/DataTableControl/index.tsx @@ -163,6 +163,7 @@ const DataTableTemporalHeaderCell = ({ columnName, onTimeColumnChange, datasourceId, + isOriginalTimeColumn, }: { columnName: string; onTimeColumnChange: ( @@ -170,15 +171,12 @@ const DataTableTemporalHeaderCell = ({ columnType: FormatPickerValue, ) => void; datasourceId?: string; + isOriginalTimeColumn: boolean; }) => { const theme = useTheme(); - const [isOriginalTimeColumn, setIsOriginalTimeColumn] = useState( - getTimeColumns(datasourceId).includes(columnName), - ); const onChange = (e: any) => { onTimeColumnChange(columnName, e.target.value); - setIsOriginalTimeColumn(getTimeColumns(datasourceId).includes(columnName)); }; const overlayContent = useMemo( @@ -313,6 +311,8 @@ export const useTableColumns = ( colType === GenericDataType.TEMPORAL ? originalFormattedTimeColumns.indexOf(key) : -1; + const isOriginalTimeColumn = + originalFormattedTimeColumns.includes(key); return { id: key, accessor: row => row[key], @@ -324,6 +324,7 @@ export const useTableColumns = ( columnName={key} datasourceId={datasourceId} onTimeColumnChange={onTimeColumnChange} + isOriginalTimeColumn={isOriginalTimeColumn} /> ) : ( key diff --git a/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.tsx b/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.tsx index 99b7059c632db..bfba9cf980011 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.tsx @@ -31,13 +31,12 @@ import { setItem, LocalStorageKeys, } from 'src/utils/localStorageHelpers'; -import { ResultsPane, SamplesPane, TableControlsWrapper } from './components'; -import { DataTablesPaneProps } from './types'; - -enum ResultTypes { - Results = 'results', - Samples = 'samples', -} +import { + SamplesPane, + TableControlsWrapper, + useResultsPane, +} from './components'; +import { DataTablesPaneProps, ResultTypes } from './types'; const SouthPane = styled.div` ${({ theme }) => ` @@ -114,7 +113,7 @@ export const DataTablesPane = ({ if ( panelOpen && - activeTabKey === ResultTypes.Results && + activeTabKey.startsWith(ResultTypes.Results) && chartStatus === 'rendered' ) { setIsRequest({ @@ -187,6 +186,35 @@ export const DataTablesPane = ({ ); }, [handleCollapseChange, panelOpen, theme.colors.grayscale.base]); + const queryResultsPanes = useResultsPane({ + errorMessage, + queryFormData, + queryForce, + ownState, + isRequest: isRequest.results, + actions, + isVisible: ResultTypes.Results === activeTabKey, + }).map((pane, idx) => { + if (idx === 0) { + return ( + + {pane} + + ); + } + if (idx > 0) { + return ( + + {pane} + + ); + } + return null; + }); + return ( - - - + {queryResultsPanes} diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/ResultsPaneOnDashboard.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/ResultsPaneOnDashboard.tsx new file mode 100644 index 0000000000000..3f27929f5cd8b --- /dev/null +++ b/superset-frontend/src/explore/components/DataTablesPane/components/ResultsPaneOnDashboard.tsx @@ -0,0 +1,69 @@ +/** + * 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 { t } from '@superset-ui/core'; +import Tabs from 'src/components/Tabs'; +import { ResultTypes, ResultsPaneProps } from '../types'; +import { useResultsPane } from './useResultsPane'; + +export const ResultsPaneOnDashboard = ({ + isRequest, + queryFormData, + queryForce, + ownState, + errorMessage, + actions, + isVisible, + dataSize = 50, +}: ResultsPaneProps) => { + const resultsPanes = useResultsPane({ + errorMessage, + queryFormData, + queryForce, + ownState, + isRequest, + actions, + dataSize, + isVisible, + }); + if (resultsPanes.length === 1) { + return resultsPanes[0]; + } + + const panes = resultsPanes.map((pane, idx) => { + if (idx === 0) { + return ( + + {pane} + + ); + } + + return ( + + {pane} + + ); + }); + + return {panes} ; +}; diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/SamplesPane.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/SamplesPane.tsx index 1997acf596ede..8b1137334b9de 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/components/SamplesPane.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane/components/SamplesPane.tsx @@ -41,6 +41,7 @@ export const SamplesPane = ({ queryForce, actions, dataSize = 50, + isVisible, }: SamplesPaneProps) => { const [filterText, setFilterText] = useState(''); const [data, setData] = useState[][]>([]); @@ -90,7 +91,7 @@ export const SamplesPane = ({ coltypes, data, datasourceId, - isRequest, + isVisible, ); const filteredData = useFilteredTableData(filterText, data); diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/SingleQueryResultPane.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/SingleQueryResultPane.tsx new file mode 100644 index 0000000000000..27d312cc3ccda --- /dev/null +++ b/superset-frontend/src/explore/components/DataTablesPane/components/SingleQueryResultPane.tsx @@ -0,0 +1,73 @@ +/** + * 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, { useState } from 'react'; +import { t } from '@superset-ui/core'; +import TableView, { EmptyWrapperType } from 'src/components/TableView'; +import { + useFilteredTableData, + useTableColumns, +} from 'src/explore/components/DataTableControl'; +import { TableControls } from './DataTableControls'; +import { SingleQueryResultPaneProp } from '../types'; + +export const SingleQueryResultPane = ({ + data, + colnames, + coltypes, + datasourceId, + dataSize = 50, + isVisible, +}: SingleQueryResultPaneProp) => { + const [filterText, setFilterText] = useState(''); + + // this is to preserve the order of the columns, even if there are integer values, + // while also only grabbing the first column's keys + const columns = useTableColumns( + colnames, + coltypes, + data, + datasourceId, + isVisible, + ); + const filteredData = useFilteredTableData(filterText, data); + + return ( + <> + setFilterText(input)} + isLoading={false} + /> + + + ); +}; diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/index.ts b/superset-frontend/src/explore/components/DataTablesPane/components/index.ts index 41623cb572083..e5762494c5576 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/components/index.ts +++ b/superset-frontend/src/explore/components/DataTablesPane/components/index.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -export { ResultsPane } from './ResultsPane'; +export { ResultsPaneOnDashboard } from './ResultsPaneOnDashboard'; export { SamplesPane } from './SamplesPane'; export { TableControls, TableControlsWrapper } from './DataTableControls'; +export { useResultsPane } from './useResultsPane'; diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/ResultsPane.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/useResultsPane.tsx similarity index 50% rename from superset-frontend/src/explore/components/DataTablesPane/components/ResultsPane.tsx rename to superset-frontend/src/explore/components/DataTablesPane/components/useResultsPane.tsx index d69a244430550..20e53df849cd1 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/components/ResultsPane.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane/components/useResultsPane.tsx @@ -17,18 +17,15 @@ * under the License. */ import React, { useState, useEffect } from 'react'; -import { ensureIsArray, GenericDataType, styled, t } from '@superset-ui/core'; +import { ensureIsArray, styled, t } from '@superset-ui/core'; import Loading from 'src/components/Loading'; import { EmptyStateMedium } from 'src/components/EmptyState'; -import TableView, { EmptyWrapperType } from 'src/components/TableView'; -import { - useFilteredTableData, - useTableColumns, -} from 'src/explore/components/DataTableControl'; import { getChartDataRequest } from 'src/components/Chart/chartAction'; import { getClientErrorObject } from 'src/utils/getClientErrorObject'; +import { ResultsPaneProps, QueryResultInterface } from '../types'; +import { getQueryCount } from '../utils'; +import { SingleQueryResultPane } from './SingleQueryResultPane'; import { TableControls } from './DataTableControls'; -import { ResultsPaneProps } from '../types'; const Error = styled.pre` margin-top: ${({ theme }) => `${theme.gridUnit * 4}px`}; @@ -36,21 +33,22 @@ const Error = styled.pre` const cache = new WeakSet(); -export const ResultsPane = ({ +export const useResultsPane = ({ isRequest, queryFormData, queryForce, ownState, errorMessage, actions, + isVisible, dataSize = 50, -}: ResultsPaneProps) => { - const [filterText, setFilterText] = useState(''); - const [data, setData] = useState[][]>([]); - const [colnames, setColnames] = useState([]); - const [coltypes, setColtypes] = useState([]); +}: ResultsPaneProps): React.ReactElement[] => { + const [resultResp, setResultResp] = useState([]); const [isLoading, setIsLoading] = useState(true); const [responseError, setResponseError] = useState(''); + const queryCount = getQueryCount( + queryFormData?.viz_type || queryFormData?.vizType, + ); useEffect(() => { // it's an invalid formData when gets a errorMessage @@ -65,28 +63,7 @@ export const ResultsPane = ({ ownState, }) .then(({ json }) => { - const { colnames, coltypes } = json.result[0]; - // Only displaying the first query is currently supported - if (json.result.length > 1) { - // todo: move these code to the backend, shouldn't loop by row in FE - const data: any[] = []; - json.result.forEach((item: { data: any[] }) => { - item.data.forEach((row, i) => { - if (data[i] !== undefined) { - data[i] = { ...data[i], ...row }; - } else { - data[i] = row; - } - }); - }); - setData(data); - setColnames(colnames); - setColtypes(coltypes); - } else { - setData(ensureIsArray(json.result[0].data)); - setColnames(colnames); - setColtypes(coltypes); - } + setResultResp(ensureIsArray(json.result)); setResponseError(''); cache.add(queryFormData); if (queryForce && actions) { @@ -110,68 +87,50 @@ export const ResultsPane = ({ } }, [errorMessage]); - // this is to preserve the order of the columns, even if there are integer values, - // while also only grabbing the first column's keys - const columns = useTableColumns( - colnames, - coltypes, - data, - queryFormData.datasource, - isRequest, - ); - const filteredData = useFilteredTableData(filterText, data); - if (isLoading) { - return ; + return Array(queryCount).fill(); } if (errorMessage) { const title = t('Run a query to display results'); - return ; + return Array(queryCount).fill( + , + ); } if (responseError) { - return ( + const err = ( <> setFilterText(input)} - isLoading={isLoading} + data={[]} + columnNames={[]} + columnTypes={[]} + datasourceId={queryFormData.datasource} + onInputChange={() => {}} + isLoading={false} /> {responseError} ); + return Array(queryCount).fill(err); } - if (data.length === 0) { + if (resultResp.length === 0) { const title = t('No results were returned for this query'); - return ; + return Array(queryCount).fill( + , + ); } - return ( - <> - setFilterText(input)} - isLoading={isLoading} - /> - - - ); + return resultResp.map((result, idx) => ( + + )); }; diff --git a/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.test.tsx b/superset-frontend/src/explore/components/DataTablesPane/test/DataTablesPane.test.tsx similarity index 64% rename from superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.test.tsx rename to superset-frontend/src/explore/components/DataTablesPane/test/DataTablesPane.test.tsx index 57d599ee82b9a..c5d9d0c7bb2d3 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/DataTablesPane.test.tsx +++ b/superset-frontend/src/explore/components/DataTablesPane/test/DataTablesPane.test.tsx @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ - import React from 'react'; import userEvent from '@testing-library/user-event'; import fetchMock from 'fetch-mock'; @@ -26,56 +25,8 @@ import { screen, waitForElementToBeRemoved, } from 'spec/helpers/testing-library'; -import { DatasourceType } from '@superset-ui/core'; -import { exploreActions } from 'src/explore/actions/exploreActions'; -import { ChartStatus } from 'src/explore/types'; -import { DataTablesPane } from '.'; - -const createProps = () => ({ - queryFormData: { - viz_type: 'heatmap', - datasource: '34__table', - slice_id: 456, - url_params: {}, - time_range: 'Last week', - all_columns_x: 'source', - all_columns_y: 'target', - metric: 'sum__value', - adhoc_filters: [], - row_limit: 10000, - linear_color_scheme: 'blue_white_yellow', - xscale_interval: null, - yscale_interval: null, - canvas_image_rendering: 'pixelated', - normalize_across: 'heatmap', - left_margin: 'auto', - bottom_margin: 'auto', - y_axis_bounds: [null, null], - y_axis_format: 'SMART_NUMBER', - show_perc: true, - sort_x_axis: 'alpha_asc', - sort_y_axis: 'alpha_asc', - extra_form_data: {}, - }, - queryForce: false, - chartStatus: 'rendered' as ChartStatus, - onCollapseChange: jest.fn(), - queriesResponse: [ - { - colnames: [], - }, - ], - datasource: { - id: 0, - name: '', - type: DatasourceType.Table, - columns: [], - metrics: [], - columnFormats: {}, - verboseMap: {}, - }, - actions: exploreActions, -}); +import { DataTablesPane } from '..'; +import { createDataTablesPaneProps } from './fixture'; describe('DataTablesPane', () => { // Collapsed/expanded state depends on local storage @@ -89,7 +40,7 @@ describe('DataTablesPane', () => { }); test('Rendering DataTablesPane correctly', () => { - const props = createProps(); + const props = createDataTablesPaneProps(0); render(, { useRedux: true }); expect(screen.getByText('Results')).toBeVisible(); expect(screen.getByText('Samples')).toBeVisible(); @@ -97,7 +48,7 @@ describe('DataTablesPane', () => { }); test('Collapse/Expand buttons', async () => { - const props = createProps(); + const props = createDataTablesPaneProps(0); render(, { useRedux: true, }); @@ -112,7 +63,7 @@ describe('DataTablesPane', () => { }); test('Should show tabs: View results', async () => { - const props = createProps(); + const props = createDataTablesPaneProps(0); render(, { useRedux: true, }); @@ -121,9 +72,8 @@ describe('DataTablesPane', () => { expect(await screen.findByLabelText('Collapse data panel')).toBeVisible(); localStorage.clear(); }); - test('Should show tabs: View samples', async () => { - const props = createProps(); + const props = createDataTablesPaneProps(0); render(, { useRedux: true, }); @@ -146,31 +96,10 @@ describe('DataTablesPane', () => { }, ); const copyToClipboardSpy = jest.spyOn(copyUtils, 'default'); - const props = createProps(); - render( - , - { - useRedux: true, - initialState: { - explore: { - originalFormattedTimeColumns: { - '34__table': ['__timestamp'], - }, - }, - }, - }, - ); + const props = createDataTablesPaneProps(456); + render(, { + useRedux: true, + }); userEvent.click(screen.getByText('Results')); expect(await screen.findByText('1 row')).toBeVisible(); @@ -184,7 +113,7 @@ describe('DataTablesPane', () => { test('Search table', async () => { fetchMock.post( - 'glob:*/api/v1/chart/data?form_data=%7B%22slice_id%22%3A456%7D', + 'glob:*/api/v1/chart/data?form_data=%7B%22slice_id%22%3A789%7D', { result: [ { @@ -198,31 +127,10 @@ describe('DataTablesPane', () => { ], }, ); - const props = createProps(); - render( - , - { - useRedux: true, - initialState: { - explore: { - originalFormattedTimeColumns: { - '34__table': ['__timestamp'], - }, - }, - }, - }, - ); + const props = createDataTablesPaneProps(789); + render(, { + useRedux: true, + }); userEvent.click(screen.getByText('Results')); expect(await screen.findByText('2 rows')).toBeVisible(); expect(screen.getByText('Action')).toBeVisible(); diff --git a/superset-frontend/src/explore/components/DataTablesPane/test/ResultsPaneOnDashboard.test.tsx b/superset-frontend/src/explore/components/DataTablesPane/test/ResultsPaneOnDashboard.test.tsx new file mode 100644 index 0000000000000..19980ff711479 --- /dev/null +++ b/superset-frontend/src/explore/components/DataTablesPane/test/ResultsPaneOnDashboard.test.tsx @@ -0,0 +1,160 @@ +/** + * 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 fetchMock from 'fetch-mock'; +import userEvent from '@testing-library/user-event'; +import { + render, + waitForElementToBeRemoved, +} from 'spec/helpers/testing-library'; +import { exploreActions } from 'src/explore/actions/exploreActions'; +import { promiseTimeout } from '@superset-ui/core'; +import { ResultsPaneOnDashboard } from '../components'; +import { createResultsPaneOnDashboardProps } from './fixture'; + +describe('ResultsPaneOnDashboard', () => { + // render and render errorMessage + fetchMock.post( + 'end:/api/v1/chart/data?form_data=%7B%22slice_id%22%3A121%7D', + { + result: [], + }, + ); + + // force query, render and search + fetchMock.post( + 'end:/api/v1/chart/data?form_data=%7B%22slice_id%22%3A144%7D&force=true', + { + result: [ + { + data: [ + { __timestamp: 1230768000000, genre: 'Action' }, + { __timestamp: 1230768000010, genre: 'Horror' }, + ], + colnames: ['__timestamp', 'genre'], + coltypes: [2, 1], + }, + ], + }, + ); + + // error response + fetchMock.post( + 'end:/api/v1/chart/data?form_data=%7B%22slice_id%22%3A169%7D', + 400, + ); + + // multiple results pane + fetchMock.post( + 'end:/api/v1/chart/data?form_data=%7B%22slice_id%22%3A196%7D', + { + result: [ + { + data: [ + { __timestamp: 1230768000000 }, + { __timestamp: 1230768000010 }, + ], + colnames: ['__timestamp'], + coltypes: [2], + }, + { + data: [{ genre: 'Action' }, { genre: 'Horror' }], + colnames: ['genre'], + coltypes: [1], + }, + ], + }, + ); + + const setForceQuery = jest.spyOn(exploreActions, 'setForceQuery'); + + afterAll(() => { + fetchMock.reset(); + jest.resetAllMocks(); + }); + + test('render', async () => { + const props = createResultsPaneOnDashboardProps({ sliceId: 121 }); + const { findByText } = render(, { + useRedux: true, + }); + expect( + await findByText('No results were returned for this query'), + ).toBeVisible(); + }); + + test('render errorMessage', async () => { + const props = createResultsPaneOnDashboardProps({ + sliceId: 121, + errorMessage:

error

, + }); + const { findByText } = render(, { + useRedux: true, + }); + expect(await findByText('Run a query to display results')).toBeVisible(); + }); + + test('error response', async () => { + const props = createResultsPaneOnDashboardProps({ + sliceId: 169, + }); + const { findByText } = render(, { + useRedux: true, + }); + expect(await findByText('0 rows')).toBeVisible(); + expect(await findByText('Bad Request')).toBeVisible(); + }); + + test('force query, render and search', async () => { + const props = createResultsPaneOnDashboardProps({ + sliceId: 144, + queryForce: true, + }); + const { queryByText, getByPlaceholderText } = render( + , + { + useRedux: true, + }, + ); + + await promiseTimeout(() => { + expect(setForceQuery).toHaveBeenCalledTimes(1); + }, 10); + expect(queryByText('2 rows')).toBeVisible(); + expect(queryByText('Action')).toBeVisible(); + expect(queryByText('Horror')).toBeVisible(); + + userEvent.type(getByPlaceholderText('Search'), 'hor'); + await waitForElementToBeRemoved(() => queryByText('Action')); + expect(queryByText('Horror')).toBeVisible(); + expect(queryByText('Action')).not.toBeInTheDocument(); + }); + + test('multiple results pane', async () => { + const props = createResultsPaneOnDashboardProps({ + sliceId: 196, + vizType: 'mixed_timeseries', + }); + const { findByText } = render(, { + useRedux: true, + }); + expect(await findByText('Results')).toBeVisible(); + expect(await findByText('Results 2')).toBeVisible(); + }); +}); diff --git a/superset-frontend/src/explore/components/DataTablesPane/test/SamplesPane.test.tsx b/superset-frontend/src/explore/components/DataTablesPane/test/SamplesPane.test.tsx new file mode 100644 index 0000000000000..54c04c6003ba2 --- /dev/null +++ b/superset-frontend/src/explore/components/DataTablesPane/test/SamplesPane.test.tsx @@ -0,0 +1,106 @@ +/** + * 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 fetchMock from 'fetch-mock'; +import userEvent from '@testing-library/user-event'; +import { + render, + waitForElementToBeRemoved, +} from 'spec/helpers/testing-library'; +import { exploreActions } from 'src/explore/actions/exploreActions'; +import { promiseTimeout } from '@superset-ui/core'; +import { SamplesPane } from '../components'; +import { createSamplesPaneProps } from './fixture'; + +describe('SamplesPane', () => { + fetchMock.get('end:/api/v1/dataset/34/samples?force=false', { + result: { + data: [], + colnames: [], + coltypes: [], + }, + }); + + fetchMock.get('end:/api/v1/dataset/35/samples?force=true', { + result: { + data: [ + { __timestamp: 1230768000000, genre: 'Action' }, + { __timestamp: 1230768000010, genre: 'Horror' }, + ], + colnames: ['__timestamp', 'genre'], + coltypes: [2, 1], + }, + }); + + fetchMock.get('end:/api/v1/dataset/36/samples?force=false', 400); + + const setForceQuery = jest.spyOn(exploreActions, 'setForceQuery'); + + afterAll(() => { + fetchMock.reset(); + jest.resetAllMocks(); + }); + + test('render', async () => { + const props = createSamplesPaneProps({ datasourceId: 34 }); + const { findByText } = render(); + expect( + await findByText('No samples were returned for this dataset'), + ).toBeVisible(); + await promiseTimeout(() => { + expect(setForceQuery).toHaveBeenCalledTimes(0); + }, 10); + }); + + test('error response', async () => { + const props = createSamplesPaneProps({ + datasourceId: 36, + }); + const { findByText } = render(, { + useRedux: true, + }); + + expect(await findByText('Error: Bad Request')).toBeVisible(); + }); + + test('force query, render and search', async () => { + const props = createSamplesPaneProps({ + datasourceId: 35, + queryForce: true, + }); + const { queryByText, getByPlaceholderText } = render( + , + { + useRedux: true, + }, + ); + + await promiseTimeout(() => { + expect(setForceQuery).toHaveBeenCalledTimes(1); + }, 10); + expect(queryByText('2 rows')).toBeVisible(); + expect(queryByText('Action')).toBeVisible(); + expect(queryByText('Horror')).toBeVisible(); + + userEvent.type(getByPlaceholderText('Search'), 'hor'); + await waitForElementToBeRemoved(() => queryByText('Action')); + expect(queryByText('Horror')).toBeVisible(); + expect(queryByText('Action')).not.toBeInTheDocument(); + }); +}); diff --git a/superset-frontend/src/explore/components/DataTablesPane/test/fixture.tsx b/superset-frontend/src/explore/components/DataTablesPane/test/fixture.tsx new file mode 100644 index 0000000000000..d8428a227b165 --- /dev/null +++ b/superset-frontend/src/explore/components/DataTablesPane/test/fixture.tsx @@ -0,0 +1,119 @@ +/** + * 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 { DatasourceType } from '@superset-ui/core'; +import { exploreActions } from 'src/explore/actions/exploreActions'; +import { ChartStatus } from 'src/explore/types'; +import { + DataTablesPaneProps, + SamplesPaneProps, + ResultsPaneProps, +} from '../types'; + +const queryFormData = { + viz_type: 'heatmap', + datasource: '34__table', + slice_id: 456, + url_params: {}, + time_range: 'Last week', + all_columns_x: 'source', + all_columns_y: 'target', + metric: 'sum__value', + adhoc_filters: [], + row_limit: 10000, + linear_color_scheme: 'blue_white_yellow', + xscale_interval: null, + yscale_interval: null, + canvas_image_rendering: 'pixelated', + normalize_across: 'heatmap', + left_margin: 'auto', + bottom_margin: 'auto', + y_axis_bounds: [null, null], + y_axis_format: 'SMART_NUMBER', + show_perc: true, + sort_x_axis: 'alpha_asc', + sort_y_axis: 'alpha_asc', + extra_form_data: {}, +}; + +const datasource = { + id: 34, + name: '', + type: DatasourceType.Table, + columns: [], + metrics: [], + columnFormats: {}, + verboseMap: {}, +}; + +export const createDataTablesPaneProps = (sliceId: number) => + ({ + queryFormData: { + ...queryFormData, + slice_id: sliceId, + }, + datasource, + queryForce: false, + chartStatus: 'rendered' as ChartStatus, + onCollapseChange: jest.fn(), + actions: exploreActions, + } as DataTablesPaneProps); + +export const createSamplesPaneProps = ({ + datasourceId, + queryForce = false, + isRequest = true, +}: { + datasourceId: number; + queryForce?: boolean; + isRequest?: boolean; +}) => + ({ + isRequest, + datasource: { ...datasource, id: datasourceId }, + queryForce, + isVisible: true, + actions: exploreActions, + } as SamplesPaneProps); + +export const createResultsPaneOnDashboardProps = ({ + sliceId, + errorMessage, + vizType = 'table', + queryForce = false, + isRequest = true, +}: { + sliceId: number; + vizType?: string; + errorMessage?: React.ReactElement; + queryForce?: boolean; + isRequest?: boolean; +}) => + ({ + isRequest, + queryFormData: { + ...queryFormData, + slice_id: sliceId, + viz_type: vizType, + }, + queryForce, + isVisible: true, + actions: exploreActions, + errorMessage, + } as ResultsPaneProps); diff --git a/superset-frontend/src/explore/components/DataTablesPane/types.ts b/superset-frontend/src/explore/components/DataTablesPane/types.ts index f526536640c6e..4e6062ba4a256 100644 --- a/superset-frontend/src/explore/components/DataTablesPane/types.ts +++ b/superset-frontend/src/explore/components/DataTablesPane/types.ts @@ -25,6 +25,11 @@ import { import { ExploreActions } from 'src/explore/actions/exploreActions'; import { ChartStatus } from 'src/explore/types'; +export enum ResultTypes { + Results = 'results', + Samples = 'samples', +} + export interface DataTablesPaneProps { queryFormData: QueryFormData; datasource: Datasource; @@ -44,6 +49,8 @@ export interface ResultsPaneProps { errorMessage?: React.ReactElement; actions?: ExploreActions; dataSize?: number; + // reload OriginalFormattedTimeColumns from localStorage when isVisible is true + isVisible: boolean; } export interface SamplesPaneProps { @@ -52,6 +59,8 @@ export interface SamplesPaneProps { queryForce: boolean; actions?: ExploreActions; dataSize?: number; + // reload OriginalFormattedTimeColumns from localStorage when isVisible is true + isVisible: boolean; } export interface TableControlsProps { @@ -63,3 +72,17 @@ export interface TableControlsProps { columnTypes: GenericDataType[]; isLoading: boolean; } + +export interface QueryResultInterface { + colnames: string[]; + coltypes: GenericDataType[]; + data: Record[][]; +} + +export interface SingleQueryResultPaneProp extends QueryResultInterface { + // {datasource.id}__{datasource.type}, eg: 1__table + datasourceId: string; + dataSize?: number; + // reload OriginalFormattedTimeColumns from localStorage when isVisible is true + isVisible: boolean; +} diff --git a/superset-frontend/src/explore/components/DataTablesPane/utils.ts b/superset-frontend/src/explore/components/DataTablesPane/utils.ts new file mode 100644 index 0000000000000..c6394fb9b67f5 --- /dev/null +++ b/superset-frontend/src/explore/components/DataTablesPane/utils.ts @@ -0,0 +1,24 @@ +/** + * 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. + */ +const queryObjectCount = { + mixed_timeseries: 2, +}; + +export const getQueryCount = (vizType: string): number => + queryObjectCount?.[vizType] || 1; From d04357c47bec7bac49c602f3d2166375892200ad Mon Sep 17 00:00:00 2001 From: Yongjie Zhao Date: Thu, 9 Jun 2022 17:43:42 +0800 Subject: [PATCH 09/71] fix: superset-ui/core codes coverage (#20324) --- superset-frontend/jest.config.js | 2 +- .../superset-ui-chart-controls/src/types.ts | 22 +++++++-- .../src/utils/columnChoices.ts | 35 +++++++------- .../test/utils/defineSavedMetrics.test.tsx | 39 +++++++-------- .../packages/superset-ui-core/src/index.ts | 1 - .../Base.ts => query/types/Dashboard.ts} | 2 + .../superset-ui-core/src/query/types/Query.ts | 8 ++-- .../superset-ui-core/src/query/types/index.ts | 2 + .../test/query/types/Dashboard.test.ts | 47 +++++++++++++++++++ .../ui-overrides/UiOverrideRegistry.test.ts} | 5 +- 10 files changed, 117 insertions(+), 46 deletions(-) rename superset-frontend/packages/superset-ui-core/src/{dashboard/types/Base.ts => query/types/Dashboard.ts} (99%) create mode 100644 superset-frontend/packages/superset-ui-core/test/query/types/Dashboard.test.ts rename superset-frontend/packages/superset-ui-core/{src/dashboard/index.ts => test/ui-overrides/UiOverrideRegistry.test.ts} (80%) diff --git a/superset-frontend/jest.config.js b/superset-frontend/jest.config.js index 18d20a1f97043..0d66ade8b366b 100644 --- a/superset-frontend/jest.config.js +++ b/superset-frontend/jest.config.js @@ -47,7 +47,7 @@ module.exports = { 'tmp/', 'dist/', ], - coverageReporters: ['lcov', 'json-summary', 'html'], + coverageReporters: ['lcov', 'json-summary', 'html', 'text'], moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'], snapshotSerializers: ['@emotion/jest/enzyme-serializer'], globals: { diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts index a7bd128be9138..431450f1429b6 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts @@ -17,17 +17,17 @@ * specific language governing permissions and limitations * under the License. */ -import React, { ReactNode, ReactText, ReactElement } from 'react'; +import React, { ReactElement, ReactNode, ReactText } from 'react'; import type { AdhocColumn, Column, DatasourceType, JsonValue, Metric, + QueryFormColumn, QueryFormData, - QueryResponse, QueryFormMetric, - QueryFormColumn, + QueryResponse, } from '@superset-ui/core'; import { sharedControls } from './shared-controls'; import sharedControlComponents from './shared-controls/components'; @@ -437,3 +437,19 @@ export function isControlPanelSectionConfig( ): section is ControlPanelSectionConfig { return section !== null; } + +export function isDataset( + datasource: Dataset | QueryResponse | null | undefined, +): datasource is Dataset { + return !!datasource && 'columns' in datasource; +} + +export function isQueryResponse( + datasource: Dataset | QueryResponse | null | undefined, +): datasource is QueryResponse { + return ( + !!datasource && + ('results' in datasource || + datasource?.type === ('query' as DatasourceType.Query)) + ); +} diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/utils/columnChoices.ts b/superset-frontend/packages/superset-ui-chart-controls/src/utils/columnChoices.ts index 0387717ff7e3f..fd4e1fb512c45 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/utils/columnChoices.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/utils/columnChoices.ts @@ -16,8 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import { QueryResponse } from '@superset-ui/core'; -import { Dataset } from '../types'; +import { ensureIsArray, QueryResponse } from '@superset-ui/core'; +import { Dataset, isColumnMeta, isDataset, isQueryResponse } from '../types'; /** * Convert Datasource columns to column choices @@ -25,23 +25,24 @@ import { Dataset } from '../types'; export default function columnChoices( datasource?: Dataset | QueryResponse | null, ): [string, string][] { - if (datasource?.columns[0]?.hasOwnProperty('column_name')) { - return ( - (datasource as Dataset)?.columns - .map((col): [string, string] => [ - col.column_name, - col.verbose_name || col.column_name, - ]) - .sort((opt1, opt2) => - opt1[1].toLowerCase() > opt2[1].toLowerCase() ? 1 : -1, - ) || [] - ); + if (isDataset(datasource) && isColumnMeta(datasource.columns[0])) { + return datasource.columns + .map((col): [string, string] => [ + col.column_name, + col.verbose_name || col.column_name, + ]) + .sort((opt1, opt2) => + opt1[1].toLowerCase() > opt2[1].toLowerCase() ? 1 : -1, + ); } - return ( - (datasource as QueryResponse)?.columns + + if (isQueryResponse(datasource)) { + return ensureIsArray(datasource.columns) .map((col): [string, string] => [col.name, col.name]) .sort((opt1, opt2) => opt1[1].toLowerCase() > opt2[1].toLowerCase() ? 1 : -1, - ) || [] - ); + ); + } + + return []; } diff --git a/superset-frontend/packages/superset-ui-chart-controls/test/utils/defineSavedMetrics.test.tsx b/superset-frontend/packages/superset-ui-chart-controls/test/utils/defineSavedMetrics.test.tsx index 59036bf60495d..48b000ed17ffa 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/test/utils/defineSavedMetrics.test.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/test/utils/defineSavedMetrics.test.tsx @@ -26,30 +26,31 @@ import { defineSavedMetrics } from '@superset-ui/chart-controls'; describe('defineSavedMetrics', () => { it('defines saved metrics if source is a Dataset', () => { - expect( - defineSavedMetrics({ - id: 1, - metrics: [ - { - metric_name: 'COUNT(*) non-default-dataset-metric', - expression: 'COUNT(*) non-default-dataset-metric', - }, - ], - type: DatasourceType.Table, - main_dttm_col: 'test', - time_grain_sqla: 'P1D', - columns: [], - verbose_map: {}, - column_format: {}, - datasource_name: 'my_datasource', - description: 'this is my datasource', - }), - ).toEqual([ + const dataset = { + id: 1, + metrics: [ + { + metric_name: 'COUNT(*) non-default-dataset-metric', + expression: 'COUNT(*) non-default-dataset-metric', + }, + ], + type: DatasourceType.Table, + main_dttm_col: 'test', + time_grain_sqla: 'P1D', + columns: [], + verbose_map: {}, + column_format: {}, + datasource_name: 'my_datasource', + description: 'this is my datasource', + }; + expect(defineSavedMetrics(dataset)).toEqual([ { metric_name: 'COUNT(*) non-default-dataset-metric', expression: 'COUNT(*) non-default-dataset-metric', }, ]); + // @ts-ignore + expect(defineSavedMetrics({ ...dataset, metrics: undefined })).toEqual([]); }); it('returns default saved metrics if souce is a Query', () => { diff --git a/superset-frontend/packages/superset-ui-core/src/index.ts b/superset-frontend/packages/superset-ui-core/src/index.ts index 2a53112f6fe2a..4d3c4b7c95af9 100644 --- a/superset-frontend/packages/superset-ui-core/src/index.ts +++ b/superset-frontend/packages/superset-ui-core/src/index.ts @@ -22,7 +22,6 @@ export * from './utils'; export * from './types'; export * from './translation'; export * from './connection'; -export * from './dashboard'; export * from './dynamic-plugins'; export * from './query'; export * from './number-format'; diff --git a/superset-frontend/packages/superset-ui-core/src/dashboard/types/Base.ts b/superset-frontend/packages/superset-ui-core/src/query/types/Dashboard.ts similarity index 99% rename from superset-frontend/packages/superset-ui-core/src/dashboard/types/Base.ts rename to superset-frontend/packages/superset-ui-core/src/query/types/Dashboard.ts index af7ca34dde1ec..4089512de4973 100644 --- a/superset-frontend/packages/superset-ui-core/src/dashboard/types/Base.ts +++ b/superset-frontend/packages/superset-ui-core/src/query/types/Dashboard.ts @@ -127,3 +127,5 @@ export type DashboardComponentMetadata = { nativeFilters: NativeFiltersState; dataMask: DataMaskStateWithId; }; + +export default {}; diff --git a/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts b/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts index d4b672a7a3ad9..bcbedf536bc0b 100644 --- a/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts +++ b/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts @@ -363,14 +363,14 @@ export const testQuery: Query = { is_dttm: false, }, { - name: 'Column 2', + name: 'Column 3', type: DatasourceType.Query, - is_dttm: true, + is_dttm: false, }, { - name: 'Column 3', + name: 'Column 2', type: DatasourceType.Query, - is_dttm: false, + is_dttm: true, }, ], }; diff --git a/superset-frontend/packages/superset-ui-core/src/query/types/index.ts b/superset-frontend/packages/superset-ui-core/src/query/types/index.ts index 2ee4427efdf40..9c4b17edd894f 100644 --- a/superset-frontend/packages/superset-ui-core/src/query/types/index.ts +++ b/superset-frontend/packages/superset-ui-core/src/query/types/index.ts @@ -27,6 +27,7 @@ export * from './QueryResponse'; export * from './Time'; export * from './AdvancedAnalytics'; export * from './PostProcessing'; +export * from './Dashboard'; export { default as __hack_reexport_Datasource } from './Datasource'; export { default as __hack_reexport_Column } from './Column'; @@ -36,5 +37,6 @@ export { default as __hack_reexport_QueryResponse } from './QueryResponse'; export { default as __hack_reexport_QueryFormData } from './QueryFormData'; export { default as __hack_reexport_Time } from './Time'; export { default as __hack_reexport_AdvancedAnalytics } from './AdvancedAnalytics'; +export { default as __hack_reexport_Dashboard } from './Dashboard'; export default {}; diff --git a/superset-frontend/packages/superset-ui-core/test/query/types/Dashboard.test.ts b/superset-frontend/packages/superset-ui-core/test/query/types/Dashboard.test.ts new file mode 100644 index 0000000000000..ea6236338c765 --- /dev/null +++ b/superset-frontend/packages/superset-ui-core/test/query/types/Dashboard.test.ts @@ -0,0 +1,47 @@ +/** + * 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 { + isNativeFilter, + isFilterDivider, + Filter, + NativeFilterType, +} from '@superset-ui/core'; + +test('should do native filter type guard', () => { + const dummyFilter: Filter = { + cascadeParentIds: [], + defaultDataMask: {}, + id: 'dummyID', + name: 'dummyName', + scope: { rootPath: [], excluded: [] }, + filterType: 'dummyType', + targets: [{}], + controlValues: {}, + type: NativeFilterType.NATIVE_FILTER, + description: 'dummyDesc', + }; + expect(isNativeFilter(dummyFilter)).toBeTruthy(); + expect( + isFilterDivider({ + ...dummyFilter, + type: NativeFilterType.DIVIDER, + title: 'dummyTitle', + }), + ).toBeTruthy(); +}); diff --git a/superset-frontend/packages/superset-ui-core/src/dashboard/index.ts b/superset-frontend/packages/superset-ui-core/test/ui-overrides/UiOverrideRegistry.test.ts similarity index 80% rename from superset-frontend/packages/superset-ui-core/src/dashboard/index.ts rename to superset-frontend/packages/superset-ui-core/test/ui-overrides/UiOverrideRegistry.test.ts index 10b785cba8770..6e440551416dc 100644 --- a/superset-frontend/packages/superset-ui-core/src/dashboard/index.ts +++ b/superset-frontend/packages/superset-ui-core/test/ui-overrides/UiOverrideRegistry.test.ts @@ -16,5 +16,8 @@ * specific language governing permissions and limitations * under the License. */ +import { getUiOverrideRegistry } from '@superset-ui/core'; -export * from './types/Base'; +test('should get instance of getUiOverrideRegistry', () => { + expect(getUiOverrideRegistry().name).toBe('UiOverrideRegistry'); +}); From 354a89950c4d001da3e107f60788cea873bd6bf6 Mon Sep 17 00:00:00 2001 From: Kamil Gabryjelski Date: Thu, 9 Jun 2022 11:54:09 +0200 Subject: [PATCH 10/71] feat(explore): Denormalize form data in echarts, world map and nvd3 bar and line charts (#20313) * feat(explore): Apply denormalize form data function to echarts and world map * Denormalize form data in mixed timeseries * Add dist bar chart --- .../src/controlPanel.ts | 4 ++++ .../src/Bar/controlPanel.ts | 5 +++++ .../src/DistBar/controlPanel.ts | 12 +++++++++- .../src/Line/controlPanel.ts | 5 +++++ .../src/BoxPlot/controlPanel.ts | 13 ++++++++++- .../src/Funnel/controlPanel.tsx | 5 +++++ .../src/Gauge/controlPanel.tsx | 6 ++++- .../src/Graph/controlPanel.tsx | 4 ++++ .../src/MixedTimeseries/controlPanel.tsx | 22 ++++++++++++++++++- .../src/Radar/controlPanel.tsx | 5 +++++ .../src/Timeseries/Area/controlPanel.tsx | 5 +++++ .../Timeseries/Regular/Bar/controlPanel.tsx | 5 +++++ .../Regular/Scatter/controlPanel.tsx | 5 +++++ .../src/Timeseries/Regular/controlPanel.tsx | 1 + .../src/Timeseries/Step/controlPanel.tsx | 5 +++++ .../src/Timeseries/controlPanel.tsx | 5 +++++ .../src/Tree/controlPanel.tsx | 4 ++++ .../src/Treemap/controlPanel.tsx | 5 +++++ 18 files changed, 112 insertions(+), 4 deletions(-) diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/controlPanel.ts b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/controlPanel.ts index 93fc1ab1c9c02..f9f7dfb09dc2e 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/controlPanel.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/controlPanel.ts @@ -152,6 +152,10 @@ const config: ControlPanelConfig = { Boolean(controls?.color_by.value === ColorBy.country), }, }, + denormalizeFormData: formData => ({ + ...formData, + metrics: formData.standardizedFormData.standardizedState.metrics, + }), }; export default config; diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bar/controlPanel.ts b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bar/controlPanel.ts index 61d3f14add90a..91af47f1f7934 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bar/controlPanel.ts +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bar/controlPanel.ts @@ -122,6 +122,11 @@ const config: ControlPanelConfig = { timeSeriesSection[1], sections.annotations, ], + denormalizeFormData: formData => ({ + ...formData, + metrics: formData.standardizedFormData.standardizedState.metrics, + groupby: formData.standardizedFormData.standardizedState.columns, + }), }; export default config; diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/DistBar/controlPanel.ts b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/DistBar/controlPanel.ts index 278743d472749..5d526b43e38d9 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/DistBar/controlPanel.ts +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/DistBar/controlPanel.ts @@ -106,7 +106,6 @@ const config: ControlPanelConfig = { ], controlOverrides: { groupby: { - label: t('Dimensions'), validators: [validateNonEmpty], mapStateToProps: (state, controlState) => { const groupbyProps = @@ -134,6 +133,17 @@ const config: ControlPanelConfig = { rerender: ['groupby'], }, }, + denormalizeFormData: formData => { + const columns = + formData.standardizedFormData.standardizedState.columns.filter( + col => !ensureIsArray(formData.groupby).includes(col), + ); + return { + ...formData, + metrics: formData.standardizedFormData.standardizedState.metrics, + columns, + }; + }, }; export default config; diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Line/controlPanel.ts b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Line/controlPanel.ts index 5b277cf6718ca..fa4738ebfd2e2 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Line/controlPanel.ts +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Line/controlPanel.ts @@ -96,6 +96,11 @@ const config: ControlPanelConfig = { default: 50000, }, }, + denormalizeFormData: formData => ({ + ...formData, + metrics: formData.standardizedFormData.standardizedState.metrics, + groupby: formData.standardizedFormData.standardizedState.columns, + }), }; export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/controlPanel.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/controlPanel.ts index f8e5cbb62950f..b92c289bb43e0 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/controlPanel.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/controlPanel.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { t } from '@superset-ui/core'; +import { ensureIsArray, t } from '@superset-ui/core'; import { D3_FORMAT_DOCS, D3_FORMAT_OPTIONS, @@ -136,5 +136,16 @@ const config: ControlPanelConfig = { ), }, }, + denormalizeFormData: formData => { + const groupby = + formData.standardizedFormData.standardizedState.columns.filter( + col => !ensureIsArray(formData.columns).includes(col), + ); + return { + ...formData, + metrics: formData.standardizedFormData.standardizedState.metrics, + groupby, + }; + }, }; export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/controlPanel.tsx index fe2269cf89c05..39ce57d498eb0 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/controlPanel.tsx @@ -143,6 +143,11 @@ const config: ControlPanelConfig = { }, }; }, + denormalizeFormData: formData => ({ + ...formData, + metric: formData.standardizedFormData.standardizedState.metrics[0], + groupby: formData.standardizedFormData.standardizedState.columns, + }), }; export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/controlPanel.tsx index ff03da4153b18..d9dbb3025e8b2 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/controlPanel.tsx @@ -39,7 +39,6 @@ const config: ControlPanelConfig = { name: 'groupby', config: { ...sharedControls.groupby, - label: t('Dimensions'), description: t('Columns to group by'), }, }, @@ -309,6 +308,11 @@ const config: ControlPanelConfig = { ], }, ], + denormalizeFormData: formData => ({ + ...formData, + metric: formData.standardizedFormData.standardizedState.metrics[0], + groupby: formData.standardizedFormData.standardizedState.columns, + }), }; export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/controlPanel.tsx index cb2f586110177..39547f683a8da 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/controlPanel.tsx @@ -320,6 +320,10 @@ const controlPanel: ControlPanelConfig = { ], }, ], + denormalizeFormData: formData => ({ + ...formData, + metric: formData.standardizedFormData.standardizedState.metrics[0], + }), }; export default controlPanel; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx index 8566fa599921c..74c0ac8890261 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx @@ -17,7 +17,12 @@ * under the License. */ import React from 'react'; -import { FeatureFlag, isFeatureEnabled, t } from '@superset-ui/core'; +import { + ensureIsArray, + FeatureFlag, + isFeatureEnabled, + t, +} from '@superset-ui/core'; import { cloneDeep } from 'lodash'; import { ControlPanelConfig, @@ -435,6 +440,21 @@ const config: ControlPanelConfig = { ], }, ], + denormalizeFormData: formData => { + const groupby = + formData.standardizedFormData.standardizedState.columns.filter( + col => !ensureIsArray(formData.groupby_b).includes(col), + ); + const metrics = + formData.standardizedFormData.standardizedState.metrics.filter( + metric => !ensureIsArray(formData.metrics_b).includes(metric), + ); + return { + ...formData, + metrics, + groupby, + }; + }, }; export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/controlPanel.tsx index d24497280ae6b..d78d935a79109 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/controlPanel.tsx @@ -210,6 +210,11 @@ const config: ControlPanelConfig = { ], }, ], + denormalizeFormData: formData => ({ + ...formData, + metrics: formData.standardizedFormData.standardizedState.metrics, + groupby: formData.standardizedFormData.standardizedState.columns, + }), }; export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx index e43dda890386b..c2aeb2916b47e 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx @@ -312,6 +312,11 @@ const config: ControlPanelConfig = { default: rowLimit, }, }, + denormalizeFormData: formData => ({ + ...formData, + metrics: formData.standardizedFormData.standardizedState.metrics, + groupby: formData.standardizedFormData.standardizedState.columns, + }), }; export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx index 8716bffe2d389..f0c8aa52ac0ea 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx @@ -398,6 +398,11 @@ const config: ControlPanelConfig = { default: rowLimit, }, }, + denormalizeFormData: formData => ({ + ...formData, + metrics: formData.standardizedFormData.standardizedState.metrics, + groupby: formData.standardizedFormData.standardizedState.columns, + }), }; export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx index abc5e9a29e724..52e799309890d 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx @@ -223,6 +223,11 @@ const config: ControlPanelConfig = { default: rowLimit, }, }, + denormalizeFormData: formData => ({ + ...formData, + metrics: formData.standardizedFormData.standardizedState.metrics, + groupby: formData.standardizedFormData.standardizedState.columns, + }), }; export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/controlPanel.tsx index eaf3cb2615fd3..c56c4a2ab20ab 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/controlPanel.tsx @@ -243,6 +243,7 @@ const config: ControlPanelConfig = { denormalizeFormData: formData => ({ ...formData, metrics: formData.standardizedFormData.standardizedState.metrics, + groupby: formData.standardizedFormData.standardizedState.columns, }), }; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx index b8d3a31b2c295..94d179c5a4ab7 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx @@ -296,6 +296,11 @@ const config: ControlPanelConfig = { default: rowLimit, }, }, + denormalizeFormData: formData => ({ + ...formData, + metrics: formData.standardizedFormData.standardizedState.metrics, + groupby: formData.standardizedFormData.standardizedState.columns, + }), }; export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/controlPanel.tsx index 8f22acadeefc3..843affc3d07e4 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/controlPanel.tsx @@ -299,6 +299,11 @@ const config: ControlPanelConfig = { default: rowLimit, }, }, + denormalizeFormData: formData => ({ + ...formData, + metrics: formData.standardizedFormData.standardizedState.metrics, + groupby: formData.standardizedFormData.standardizedState.columns, + }), }; export default config; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Tree/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Tree/controlPanel.tsx index cd48e0f636e0b..097a882fa3f2d 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Tree/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Tree/controlPanel.tsx @@ -285,6 +285,10 @@ const controlPanel: ControlPanelConfig = { ], }, ], + denormalizeFormData: formData => ({ + ...formData, + metric: formData.standardizedFormData.standardizedState.metrics[0], + }), }; export default controlPanel; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/controlPanel.tsx index 63ca40225ffe6..8887377d2de51 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Treemap/controlPanel.tsx @@ -137,6 +137,11 @@ const config: ControlPanelConfig = { ], }, ], + denormalizeFormData: formData => ({ + ...formData, + metric: formData.standardizedFormData.standardizedState.metrics[0], + groupby: formData.standardizedFormData.standardizedState.columns, + }), }; export default config; From 07b4a7159dd293061b83c671ad64cc51c928a199 Mon Sep 17 00:00:00 2001 From: Stephen Liu <750188453@qq.com> Date: Thu, 9 Jun 2022 22:59:58 +0800 Subject: [PATCH 11/71] fix(chart): chart gets cut off on the dashboard (#20315) * fix(chart): chart gets cut off on the dashboard * add some failsafe * address comment --- .../src/dashboard/components/gridComponents/Chart.jsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx index 5232f51e4d597..fc3f6d39b34a0 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx @@ -224,9 +224,14 @@ export default class Chart extends React.Component { } getHeaderHeight() { - return ( - (this.headerRef && this.headerRef.offsetHeight) || DEFAULT_HEADER_HEIGHT - ); + if (this.headerRef) { + const computedStyle = getComputedStyle(this.headerRef).getPropertyValue( + 'margin-bottom', + ); + const marginBottom = parseInt(computedStyle, 10) || 0; + return this.headerRef.offsetHeight + marginBottom; + } + return DEFAULT_HEADER_HEIGHT; } setDescriptionRef(ref) { From ec331e683e03e2422e956729f3f32a2442f7d82c Mon Sep 17 00:00:00 2001 From: Beto Dealmeida Date: Thu, 9 Jun 2022 15:34:49 -0700 Subject: [PATCH 12/71] feat: Databricks native driver (#20320) --- docs/docs/databases/databricks.mdx | 49 +++++++++++++++++++------- setup.py | 5 ++- superset/db_engine_specs/databricks.py | 6 ++++ 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/docs/docs/databases/databricks.mdx b/docs/docs/databases/databricks.mdx index 9c8ddafebdd36..4070960ce2af5 100644 --- a/docs/docs/databases/databricks.mdx +++ b/docs/docs/databases/databricks.mdx @@ -7,16 +7,12 @@ version: 1 ## Databricks -To connect to Databricks, first install [databricks-dbapi](https://pypi.org/project/databricks-dbapi/) with the optional SQLAlchemy dependencies: +Databricks now offer a native DB API 2.0 driver, `databricks-sql-connector`, that can be used with the `sqlalchemy-databricks` dialect. You can install both with: ```bash -pip install databricks-dbapi[sqlalchemy] +pip install "superset[databricks]" ``` -There are two ways to connect to Databricks: using a Hive connector or an ODBC connector. Both ways work similarly, but only ODBC can be used to connect to [SQL endpoints](https://docs.databricks.com/sql/admin/sql-endpoints.html). - -### Hive - To use the Hive connector you need the following information from your cluster: - Server hostname @@ -27,15 +23,44 @@ These can be found under "Configuration" -> "Advanced Options" -> "JDBC/ODBC". You also need an access token from "Settings" -> "User Settings" -> "Access Tokens". -Once you have all this information, add a database of type "Databricks (Hive)" in Superset, and use the following SQLAlchemy URI: +Once you have all this information, add a database of type "Databricks Native Connector" and use the following SQLAlchemy URI: ``` -databricks+pyhive://token:{access token}@{server hostname}:{port}/{database name} +databricks+connector://token:{access_token}@{server_hostname}:{port}/{database_name} ``` You also need to add the following configuration to "Other" -> "Engine Parameters", with your HTTP path: +```json +{ + "connect_args": {"http_path": "sql/protocolv1/o/****"}, + "http_headers": [["User-Agent", "Apache Superset"]] +} ``` + +The `User-Agent` header is optional, but helps Databricks identify traffic from Superset. If you need to use a different header please reach out to Databricks and let them know. + +## Older driver + +Originally Superset used `databricks-dbapi` to connect to Databricks. You might want to try it if you're having problems with the official Databricks connector: + +```bash +pip install "databricks-dbapi[sqlalchemy]" +``` + +There are two ways to connect to Databricks when using `databricks-dbapi`: using a Hive connector or an ODBC connector. Both ways work similarly, but only ODBC can be used to connect to [SQL endpoints](https://docs.databricks.com/sql/admin/sql-endpoints.html). + +### Hive + +To connect to a Hive cluster add a database of type "Databricks Interactive Cluster" in Superset, and use the following SQLAlchemy URI: + +``` +databricks+pyhive://token:{access_token}@{server_hostname}:{port}/{database_name} +``` + +You also need to add the following configuration to "Other" -> "Engine Parameters", with your HTTP path: + +```json {"connect_args": {"http_path": "sql/protocolv1/o/****"}} ``` @@ -43,15 +68,15 @@ You also need to add the following configuration to "Other" -> "Engine Parameter For ODBC you first need to install the [ODBC drivers for your platform](https://databricks.com/spark/odbc-drivers-download). -For a regular connection use this as the SQLAlchemy URI: +For a regular connection use this as the SQLAlchemy URI after selecting either "Databricks Interactive Cluster" or "Databricks SQL Endpoint" for the database, depending on your use case: ``` -databricks+pyodbc://token:{access token}@{server hostname}:{port}/{database name} +databricks+pyodbc://token:{access_token}@{server_hostname}:{port}/{database_name} ``` And for the connection arguments: -``` +```json {"connect_args": {"http_path": "sql/protocolv1/o/****", "driver_path": "/path/to/odbc/driver"}} ``` @@ -62,6 +87,6 @@ The driver path should be: For a connection to a SQL endpoint you need to use the HTTP path from the endpoint: -``` +```json {"connect_args": {"http_path": "/sql/1.0/endpoints/****", "driver_path": "/path/to/odbc/driver"}} ``` diff --git a/setup.py b/setup.py index 556e1709859aa..28a220271bcbf 100644 --- a/setup.py +++ b/setup.py @@ -129,7 +129,10 @@ def get_git_sha() -> str: "cockroachdb": ["cockroachdb>=0.3.5, <0.4"], "cors": ["flask-cors>=2.0.0"], "crate": ["crate[sqlalchemy]>=0.26.0, <0.27"], - "databricks": ["databricks-dbapi[sqlalchemy]>=0.5.0, <0.6"], + "databricks": [ + "databricks-sql-connector>=2.0.2, <3", + "sqlalchemy-databricks>=0.2.0", + ], "db2": ["ibm-db-sa>=0.3.5, <0.4"], "dremio": ["sqlalchemy-dremio>=1.1.5, <1.3"], "drill": ["sqlalchemy-drill==0.1.dev"], diff --git a/superset/db_engine_specs/databricks.py b/superset/db_engine_specs/databricks.py index f5f46bf491200..d010b520d0b00 100644 --- a/superset/db_engine_specs/databricks.py +++ b/superset/db_engine_specs/databricks.py @@ -65,3 +65,9 @@ def convert_dttm( @classmethod def epoch_to_dttm(cls) -> str: return HiveEngineSpec.epoch_to_dttm() + + +class DatabricksNativeEngineSpec(DatabricksODBCEngineSpec): + engine = "databricks" + engine_name = "Databricks Native Connector" + driver = "connector" From 72e5e57a6c04ee222a666a877622e6f983d18ff6 Mon Sep 17 00:00:00 2001 From: "Michael S. Molina" <70410625+michael-s-molina@users.noreply.github.com> Date: Fri, 10 Jun 2022 08:58:24 -0300 Subject: [PATCH 13/71] chore: Updates the final steps of the release README (#20318) * chore: Updates the final steps of the release README * Apply suggestions from code review Co-authored-by: Yongjie Zhao Co-authored-by: Yongjie Zhao --- RELEASING/README.md | 40 +++++++++++++++++++++++++++++++++++++--- scripts/pypi_push.sh | 33 --------------------------------- 2 files changed, 37 insertions(+), 36 deletions(-) delete mode 100755 scripts/pypi_push.sh diff --git a/RELEASING/README.md b/RELEASING/README.md index 470c890ba9a7d..1d3285e0ffc32 100644 --- a/RELEASING/README.md +++ b/RELEASING/README.md @@ -422,13 +422,47 @@ with the changes on `CHANGELOG.md` and `UPDATING.md`. ### Publishing a Convenience Release to PyPI -Using the final release tarball, unpack it and run `./pypi_push.sh`. -This script will build the JavaScript bundle and echo the twine command -allowing you to publish to PyPI. You may need to ask a fellow committer to grant +Extract the release to the `/tmp` folder to build the PiPY release. Files in the `/tmp` folder will be automatically deleted by the OS. + +```bash +mkdir -p /tmp/superset && cd /tmp/superset +tar xfvz ~/svn/superset/${SUPERSET_VERSION}/${SUPERSET_RELEASE_TARBALL} +``` + +Create a virtual environment and install the dependencies + +```bash +cd ${SUPERSET_RELEASE_RC} +python3 -m venv venv +source venv/bin/activate +pip install -r requirements/base.txt +pip install twine +``` + +Create the distribution + +```bash +cd superset-frontend/ +npm ci && npm run build +cd ../ +flask fab babel-compile --target superset/translations +python setup.py sdist +``` + +Publish to PyPI + +You may need to ask a fellow committer to grant you access to it if you don't have access already. Make sure to create an account first if you don't have one, and reference your username while requesting access to push packages. +```bash +twine upload dist/apache-superset-${SUPERSET_VERSION}.tar.gz + +# Set your username to token +# Set your password to the token value, including the pypi- prefix +``` + ### Announcing Once it's all done, an [ANNOUNCE] thread announcing the release to the dev@ mailing list is the final step. diff --git a/scripts/pypi_push.sh b/scripts/pypi_push.sh deleted file mode 100755 index 881d823fa86af..0000000000000 --- a/scripts/pypi_push.sh +++ /dev/null @@ -1,33 +0,0 @@ -# -# 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. -# -# Make sure you've followed the instructions on `RELEASING/README.md` -# and are on the correct branch -cd ${SUPERSET_REPO_DIR} -git branch -rm superset/static/assets/* -cd superset-frontend/ -npm ci && npm run build -cd ../ -echo "----------------------" -echo "Compiling translations" -echo "----------------------" -flask fab babel-compile --target superset/translations -echo "----------------------" -echo "Creating distribution " -echo "----------------------" -python setup.py sdist -echo "RUN: twine upload dist/apache-superset-${SUPERSET_VERSION}.tar.gz" From d0165b617b36e5b4d67262f1396c974edba65760 Mon Sep 17 00:00:00 2001 From: Cody Leff Date: Fri, 10 Jun 2022 09:57:32 -0600 Subject: [PATCH 14/71] chore(dashboard): update Edit Dashboard side panel tabs (#20337) * Reorder Dashboard Edit tabs and rename 'Components' tab to 'Layout Elements'. * Add tests for BuilderComponentPane. * Fix Cypress tests, capitalization, test nesting. --- .../integration/dashboard/edit_mode.test.js | 5 --- .../integration/dashboard/markdown.test.ts | 5 +++ .../BuilderComponentPane.test.tsx | 35 ++++++++++++++++ .../index.tsx} | 42 +++++++++---------- 4 files changed, 61 insertions(+), 26 deletions(-) create mode 100644 superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx rename superset-frontend/src/dashboard/components/{BuilderComponentPane.tsx => BuilderComponentPane/index.tsx} (86%) diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard/edit_mode.test.js b/superset-frontend/cypress-base/cypress/integration/dashboard/edit_mode.test.js index 7a3b82705cebd..10b8a4a40de1f 100644 --- a/superset-frontend/cypress-base/cypress/integration/dashboard/edit_mode.test.js +++ b/superset-frontend/cypress-base/cypress/integration/dashboard/edit_mode.test.js @@ -45,11 +45,6 @@ describe('Dashboard edit mode', () => { .should('not.exist'); }); - cy.get('[data-test="dashboard-builder-component-pane-tabs-navigation"]') - .find('.ant-tabs-tab') - .last() - .click(); - // find box plot is available from list cy.get('[data-test="dashboard-charts-filter-search-input"]').type( 'Box plot', diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard/markdown.test.ts b/superset-frontend/cypress-base/cypress/integration/dashboard/markdown.test.ts index 2964241b184a2..a27382933b532 100644 --- a/superset-frontend/cypress-base/cypress/integration/dashboard/markdown.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/dashboard/markdown.test.ts @@ -29,6 +29,11 @@ describe('Dashboard edit markdown', () => { .find('[aria-label="Edit dashboard"]') .click(); + cy.get('[data-test="dashboard-builder-component-pane-tabs-navigation"]') + .find('.ant-tabs-tab') + .last() + .click(); + // lazy load - need to open dropdown for the scripts to load cy.get('.header-with-actions').find('[aria-label="more-horiz"]').click(); cy.get('[data-test="grid-row-background--transparent"]') diff --git a/superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx b/superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx new file mode 100644 index 0000000000000..a5ff5b1314f3d --- /dev/null +++ b/superset-frontend/src/dashboard/components/BuilderComponentPane/BuilderComponentPane.test.tsx @@ -0,0 +1,35 @@ +/** + * 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 { render, screen } from 'spec/helpers/testing-library'; +import BuilderComponentPane from '.'; + +jest.mock('src/dashboard/containers/SliceAdder'); + +test('BuilderComponentPane has correct tabs in correct order', () => { + render(); + const tabs = screen.getAllByRole('tab'); + expect(tabs).toHaveLength(2); + expect(tabs[0]).toHaveTextContent('Charts'); + expect(tabs[1]).toHaveTextContent('Layout elements'); + expect(screen.getByRole('tab', { selected: true })).toHaveTextContent( + 'Charts', + ); +}); diff --git a/superset-frontend/src/dashboard/components/BuilderComponentPane.tsx b/superset-frontend/src/dashboard/components/BuilderComponentPane/index.tsx similarity index 86% rename from superset-frontend/src/dashboard/components/BuilderComponentPane.tsx rename to superset-frontend/src/dashboard/components/BuilderComponentPane/index.tsx index 9d2c2fba5ad11..4e47e161e4c19 100644 --- a/superset-frontend/src/dashboard/components/BuilderComponentPane.tsx +++ b/superset-frontend/src/dashboard/components/BuilderComponentPane/index.tsx @@ -24,15 +24,15 @@ import { ParentSize } from '@vx/responsive'; import { t, styled } from '@superset-ui/core'; -import NewColumn from './gridComponents/new/NewColumn'; -import NewDivider from './gridComponents/new/NewDivider'; -import NewHeader from './gridComponents/new/NewHeader'; -import NewRow from './gridComponents/new/NewRow'; -import NewTabs from './gridComponents/new/NewTabs'; -import NewMarkdown from './gridComponents/new/NewMarkdown'; -import SliceAdder from '../containers/SliceAdder'; -import dashboardComponents from '../../visualizations/presets/dashboardComponents'; -import NewDynamicComponent from './gridComponents/new/NewDynamicComponent'; +import SliceAdder from 'src/dashboard/containers/SliceAdder'; +import dashboardComponents from 'src/visualizations/presets/dashboardComponents'; +import NewColumn from '../gridComponents/new/NewColumn'; +import NewDivider from '../gridComponents/new/NewDivider'; +import NewHeader from '../gridComponents/new/NewHeader'; +import NewRow from '../gridComponents/new/NewRow'; +import NewTabs from '../gridComponents/new/NewTabs'; +import NewMarkdown from '../gridComponents/new/NewMarkdown'; +import NewDynamicComponent from '../gridComponents/new/NewDynamicComponent'; export interface BCPProps { isStandalone: boolean; @@ -101,7 +101,18 @@ const BuilderComponentPane: React.FC = ({ className="tabs-components" data-test="dashboard-builder-component-pane-tabs-navigation" > - + + + + @@ -117,17 +128,6 @@ const BuilderComponentPane: React.FC = ({ /> ))} - - - ); From c842c9e2d8d2b579e514fb291def3f3b0a5860e3 Mon Sep 17 00:00:00 2001 From: Lyndsi Kay Williams <55605634+lyndsiWilliams@users.noreply.github.com> Date: Fri, 10 Jun 2022 11:55:52 -0500 Subject: [PATCH 15/71] feat(explore): Dataset Panel Options when Source = Query II (#20299) * Created/tested query dataset dropdown * Add isValidDatasourceType to @superset-ui/core and hide query dropdown * Removed isValidDatasourceType check * Remove the rest of isValidDatasourceType check --- .../components/DatasourcePanel/index.tsx | 8 +- .../DatasourceControl.test.tsx | 51 ++++++++++- .../controls/DatasourceControl/index.jsx | 88 +++++++++++++++---- 3 files changed, 122 insertions(+), 25 deletions(-) diff --git a/superset-frontend/src/explore/components/DatasourcePanel/index.tsx b/superset-frontend/src/explore/components/DatasourcePanel/index.tsx index e8ea306814107..b0042e138f4a1 100644 --- a/superset-frontend/src/explore/components/DatasourcePanel/index.tsx +++ b/superset-frontend/src/explore/components/DatasourcePanel/index.tsx @@ -307,12 +307,6 @@ export default function DataSourcePanel({ return true; }; - const isValidDatasourceType = - datasource.type === DatasourceType.Dataset || - datasource.type === DatasourceType.SlTable || - datasource.type === DatasourceType.SavedQuery || - datasource.type === DatasourceType.Query; - const mainBody = useMemo( () => ( <> @@ -327,7 +321,7 @@ export default function DataSourcePanel({ placeholder={t('Search Metrics & Columns')} />
- {isValidDatasourceType && showInfoboxCheck() && ( + {datasource.type === DatasourceType.Query && showInfoboxCheck() && ( { expect(postFormSpy).toBeCalledTimes(1); }); + +test('Should open a different menu when datasource=query', () => { + const props = createProps(); + const queryProps = { + ...props, + datasource: { + ...props.datasource, + type: DatasourceType.Query, + }, + }; + render(); + + expect(screen.queryByText('Query preview')).not.toBeInTheDocument(); + expect(screen.queryByText('View in SQL Lab')).not.toBeInTheDocument(); + expect(screen.queryByText('Save as dataset')).not.toBeInTheDocument(); + + userEvent.click(screen.getByTestId('datasource-menu-trigger')); + + expect(screen.getByText('Query preview')).toBeInTheDocument(); + expect(screen.getByText('View in SQL Lab')).toBeInTheDocument(); + expect(screen.getByText('Save as dataset')).toBeInTheDocument(); +}); + +test('Click on Save as dataset', () => { + const props = createProps(); + const queryProps = { + ...props, + datasource: { + ...props.datasource, + type: DatasourceType.Query, + }, + }; + + render(, { useRedux: true }); + userEvent.click(screen.getByTestId('datasource-menu-trigger')); + userEvent.click(screen.getByText('Save as dataset')); + + // Renders a save dataset modal + const saveRadioBtn = screen.getByRole('radio', { + name: /save as new undefined/i, + }); + const overwriteRadioBtn = screen.getByRole('radio', { + name: /overwrite existing select or type dataset name/i, + }); + expect(saveRadioBtn).toBeVisible(); + expect(overwriteRadioBtn).toBeVisible(); + expect(screen.getByRole('button', { name: /save/i })).toBeVisible(); + expect(screen.getByRole('button', { name: /close/i })).toBeVisible(); +}); diff --git a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx b/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx index f3d4f8aabe0aa..15d07fd6dd9dd 100644 --- a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx +++ b/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx @@ -19,7 +19,7 @@ */ import React from 'react'; import PropTypes from 'prop-types'; -import { t, styled, withTheme } from '@superset-ui/core'; +import { t, styled, withTheme, DatasourceType } from '@superset-ui/core'; import { getUrlParam } from 'src/utils/urlUtils'; import { AntdDropdown } from 'src/components'; @@ -30,6 +30,7 @@ import { ChangeDatasourceModal, DatasourceModal, } from 'src/components/Datasource'; +import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal'; import { postForm } from 'src/explore/exploreUtils'; import Button from 'src/components/Button'; import ErrorAlert from 'src/components/ErrorMessage/ErrorAlert'; @@ -112,6 +113,8 @@ const Styles = styled.div` const CHANGE_DATASET = 'change_dataset'; const VIEW_IN_SQL_LAB = 'view_in_sql_lab'; const EDIT_DATASET = 'edit_dataset'; +const QUERY_PREVIEW = 'query_preview'; +const SAVE_AS_DATASET = 'save_as_dataset'; class DatasourceControl extends React.PureComponent { constructor(props) { @@ -126,6 +129,7 @@ class DatasourceControl extends React.PureComponent { this.toggleEditDatasourceModal = this.toggleEditDatasourceModal.bind(this); this.toggleShowDatasource = this.toggleShowDatasource.bind(this); this.handleMenuItemClick = this.handleMenuItemClick.bind(this); + this.toggleSaveDatasetModal = this.toggleSaveDatasetModal.bind(this); } onDatasourceSave(datasource) { @@ -166,25 +170,51 @@ class DatasourceControl extends React.PureComponent { })); } + toggleSaveDatasetModal() { + this.setState(({ showSaveDatasetModal }) => ({ + showSaveDatasetModal: !showSaveDatasetModal, + })); + } + handleMenuItemClick({ key }) { - if (key === CHANGE_DATASET) { - this.toggleChangeDatasourceModal(); - } - if (key === EDIT_DATASET) { - this.toggleEditDatasourceModal(); - } - if (key === VIEW_IN_SQL_LAB) { - const { datasource } = this.props; - const payload = { - datasourceKey: `${datasource.id}__${datasource.type}`, - sql: datasource.sql, - }; - postForm('/superset/sqllab/', payload); + switch (key) { + case CHANGE_DATASET: + this.toggleChangeDatasourceModal(); + break; + + case EDIT_DATASET: + this.toggleEditDatasourceModal(); + break; + + case VIEW_IN_SQL_LAB: + { + const { datasource } = this.props; + const payload = { + datasourceKey: `${datasource.id}__${datasource.type}`, + sql: datasource.sql, + }; + postForm('/superset/sqllab/', payload); + } + break; + + case QUERY_PREVIEW: + break; + + case SAVE_AS_DATASET: + this.toggleSaveDatasetModal(); + break; + + default: + break; } } render() { - const { showChangeDatasourceModal, showEditDatasourceModal } = this.state; + const { + showChangeDatasourceModal, + showEditDatasourceModal, + showSaveDatasetModal, + } = this.state; const { datasource, onChange, theme } = this.props; const isMissingDatasource = datasource.id == null; let isMissingParams = false; @@ -205,7 +235,7 @@ class DatasourceControl extends React.PureComponent { const editText = t('Edit dataset'); - const datasourceMenu = ( + const defaultDatasourceMenu = ( {this.props.isEditable && ( ); + const queryDatasourceMenu = ( + + {t('Query preview')} + {t('View in SQL Lab')} + {t('Save as dataset')} + + ); + const { health_check_message: healthCheckMessage } = datasource; let extra = {}; @@ -265,7 +303,11 @@ class DatasourceControl extends React.PureComponent { )} @@ -340,6 +382,18 @@ class DatasourceControl extends React.PureComponent { onChange={onChange} /> )} + {showSaveDatasetModal && ( + + )} ); } From 1918dc04559fcc6df369f3bf09d165561a29176e Mon Sep 17 00:00:00 2001 From: Josh Paulin Date: Fri, 10 Jun 2022 16:03:13 -0400 Subject: [PATCH 16/71] fix: Add serviceAccountName to celerybeat pods. (#19670) Co-authored-by: josh.paulin --- helm/superset/Chart.yaml | 2 +- helm/superset/templates/deployment-beat.yaml | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/helm/superset/Chart.yaml b/helm/superset/Chart.yaml index 70dd6fd162b89..16c59869dfb46 100644 --- a/helm/superset/Chart.yaml +++ b/helm/superset/Chart.yaml @@ -22,7 +22,7 @@ maintainers: - name: craig-rueda email: craig@craigrueda.com url: https://github.com/craig-rueda -version: 0.6.2 +version: 0.6.3 dependencies: - name: postgresql version: 11.1.22 diff --git a/helm/superset/templates/deployment-beat.yaml b/helm/superset/templates/deployment-beat.yaml index d46d47ee3f9c4..5587dcf343eb3 100644 --- a/helm/superset/templates/deployment-beat.yaml +++ b/helm/superset/templates/deployment-beat.yaml @@ -59,6 +59,9 @@ spec: {{ toYaml .Values.supersetCeleryBeat.podLabels | nindent 8 }} {{- end }} spec: + {{- if or (.Values.serviceAccount.create) (.Values.serviceAccountName) }} + serviceAccountName: {{ template "superset.serviceAccountName" . }} + {{- end }} securityContext: runAsUser: {{ .Values.runAsUser }} {{- if .Values.supersetCeleryBeat.initContainers }} From 11b33de61b5b28966164daddb30f5661bd109467 Mon Sep 17 00:00:00 2001 From: Reese <10563996+reesercollins@users.noreply.github.com> Date: Fri, 10 Jun 2022 16:03:48 -0400 Subject: [PATCH 17/71] feat(api): Added "kind" to dataset/ endpoint (#20113) --- docs/static/resources/openapi.json | 3 +++ superset/datasets/api.py | 1 + tests/integration_tests/datasets/api_tests.py | 1 + 3 files changed, 5 insertions(+) diff --git a/docs/static/resources/openapi.json b/docs/static/resources/openapi.json index 4eb0cc7c48660..330c629063add 100644 --- a/docs/static/resources/openapi.json +++ b/docs/static/resources/openapi.json @@ -4399,6 +4399,9 @@ "nullable": true, "type": "boolean" }, + "kind": { + "readOnly": true + }, "main_dttm_col": { "maxLength": 250, "nullable": true, diff --git a/superset/datasets/api.py b/superset/datasets/api.py index 17e99959e9675..db6136865298a 100644 --- a/superset/datasets/api.py +++ b/superset/datasets/api.py @@ -166,6 +166,7 @@ class DatasetRestApi(BaseSupersetModelRestApi): "datasource_type", "url", "extra", + "kind", ] show_columns = show_select_columns + [ "columns.type_generic", diff --git a/tests/integration_tests/datasets/api_tests.py b/tests/integration_tests/datasets/api_tests.py index 28bb617c17c19..e378811eb97b3 100644 --- a/tests/integration_tests/datasets/api_tests.py +++ b/tests/integration_tests/datasets/api_tests.py @@ -276,6 +276,7 @@ def test_get_dataset_item(self): "fetch_values_predicate": None, "filter_select_enabled": False, "is_sqllab_view": False, + "kind": "physical", "main_dttm_col": None, "offset": 0, "owners": [], From 86368dd406b9e828f31186a4b6179d24758a7d87 Mon Sep 17 00:00:00 2001 From: Multazim Deshmukh <57723564+mdeshmu@users.noreply.github.com> Date: Sat, 11 Jun 2022 01:34:39 +0530 Subject: [PATCH 18/71] fix(docker): Make Gunicorn Keepalive Adjustable (#20348) Co-authored-by: Multazim Deshmukh --- docker/run-server.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/run-server.sh b/docker/run-server.sh index 5519ff5d5c6b8..064f47b9c2cbc 100644 --- a/docker/run-server.sh +++ b/docker/run-server.sh @@ -27,6 +27,7 @@ gunicorn \ --worker-class ${SERVER_WORKER_CLASS:-gthread} \ --threads ${SERVER_THREADS_AMOUNT:-20} \ --timeout ${GUNICORN_TIMEOUT:-60} \ + --keep-alive ${GUNICORN_KEEPALIVE:-2} \ --limit-request-line ${SERVER_LIMIT_REQUEST_LINE:-0} \ --limit-request-field_size ${SERVER_LIMIT_REQUEST_FIELD_SIZE:-0} \ "${FLASK_APP}" From 8345eb4644947180e3c84ed26498abb7fa194de9 Mon Sep 17 00:00:00 2001 From: Diego Medina Date: Sun, 12 Jun 2022 11:21:30 -0300 Subject: [PATCH 19/71] fix: A newly connected database doesn't appear in the databases list if user connected database using the 'plus' button (#19967) * fix: A newly connected database doesn't appear in the databases list if user connected database using the 'plus' button * include onDatabaseAdd on successful import --- .../views/CRUD/data/database/DatabaseList.tsx | 13 ++++++ .../data/database/DatabaseModal/index.tsx | 2 +- .../src/views/components/Menu.test.tsx | 43 ++++++++++--------- .../src/views/components/MenuRight.tsx | 9 ++++ 4 files changed, 46 insertions(+), 21 deletions(-) diff --git a/superset-frontend/src/views/CRUD/data/database/DatabaseList.tsx b/superset-frontend/src/views/CRUD/data/database/DatabaseList.tsx index df4ef3cf02a40..b9c5b4a8465f6 100644 --- a/superset-frontend/src/views/CRUD/data/database/DatabaseList.tsx +++ b/superset-frontend/src/views/CRUD/data/database/DatabaseList.tsx @@ -20,6 +20,8 @@ import { SupersetClient, t, styled } from '@superset-ui/core'; import React, { useState, useMemo, useEffect } from 'react'; import rison from 'rison'; import { useSelector } from 'react-redux'; +import { useQueryParams, BooleanParam } from 'use-query-params'; + import Loading from 'src/components/Loading'; import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags'; import { useListViewResource } from 'src/views/CRUD/hooks'; @@ -91,6 +93,10 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) { state => state.user, ); + const [query, setQuery] = useQueryParams({ + databaseAdded: BooleanParam, + }); + const [databaseModalOpen, setDatabaseModalOpen] = useState(false); const [databaseCurrentlyDeleting, setDatabaseCurrentlyDeleting] = useState(null); @@ -110,6 +116,13 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) { ALLOWED_EXTENSIONS, } = useSelector(state => state.common.conf); + useEffect(() => { + if (query?.databaseAdded) { + setQuery({ databaseAdded: undefined }); + refreshData(); + } + }, [query, setQuery, refreshData]); + const openDatabaseDeleteModal = (database: DatabaseObject) => SupersetClient.get({ endpoint: `/api/v1/database/${database.id}/related_objects/`, diff --git a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.tsx b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.tsx index 392df834dd7b2..2b97ed791b792 100644 --- a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.tsx +++ b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.tsx @@ -519,7 +519,6 @@ const DatabaseModal: FunctionComponent = ({ setImportingModal(false); setPasswords({}); setConfirmedOverwrite(false); - if (onDatabaseAdd) onDatabaseAdd(); onHide(); }; @@ -652,6 +651,7 @@ const DatabaseModal: FunctionComponent = ({ confirmedOverwrite, ); if (dbId) { + if (onDatabaseAdd) onDatabaseAdd(); onClose(); addSuccessToast(t('Database connected')); } diff --git a/superset-frontend/src/views/components/Menu.test.tsx b/superset-frontend/src/views/components/Menu.test.tsx index a80a43a22f02c..0d84d2c663ccc 100644 --- a/superset-frontend/src/views/components/Menu.test.tsx +++ b/superset-frontend/src/views/components/Menu.test.tsx @@ -248,13 +248,16 @@ beforeEach(() => { test('should render', () => { useSelectorMock.mockReturnValue({ roles: user.roles }); - const { container } = render(, { useRedux: true }); + const { container } = render(, { + useRedux: true, + useQueryParams: true, + }); expect(container).toBeInTheDocument(); }); test('should render the navigation', () => { useSelectorMock.mockReturnValue({ roles: user.roles }); - render(, { useRedux: true }); + render(, { useRedux: true, useQueryParams: true }); expect(screen.getByRole('navigation')).toBeInTheDocument(); }); @@ -265,7 +268,7 @@ test('should render the brand', () => { brand: { alt, icon }, }, } = mockedProps; - render(, { useRedux: true }); + render(, { useRedux: true, useQueryParams: true }); const image = screen.getByAltText(alt); expect(image).toHaveAttribute('src', icon); }); @@ -275,7 +278,7 @@ test('should render all the top navbar menu items', () => { const { data: { menu }, } = mockedProps; - render(, { useRedux: true }); + render(, { useRedux: true, useQueryParams: true }); menu.forEach(item => { expect(screen.getByText(item.label)).toBeInTheDocument(); }); @@ -286,7 +289,7 @@ test('should render the top navbar child menu items', async () => { const { data: { menu }, } = mockedProps; - render(, { useRedux: true }); + render(, { useRedux: true, useQueryParams: true }); const sources = screen.getByText('Sources'); userEvent.hover(sources); const datasets = await screen.findByText('Datasets'); @@ -300,7 +303,7 @@ test('should render the top navbar child menu items', async () => { test('should render the dropdown items', async () => { useSelectorMock.mockReturnValue({ roles: user.roles }); - render(, { useRedux: true }); + render(, { useRedux: true, useQueryParams: true }); const dropdown = screen.getByTestId('new-dropdown-icon'); userEvent.hover(dropdown); // todo (philip): test data submenu @@ -326,14 +329,14 @@ test('should render the dropdown items', async () => { test('should render the Settings', async () => { useSelectorMock.mockReturnValue({ roles: user.roles }); - render(, { useRedux: true }); + render(, { useRedux: true, useQueryParams: true }); const settings = await screen.findByText('Settings'); expect(settings).toBeInTheDocument(); }); test('should render the Settings menu item', async () => { useSelectorMock.mockReturnValue({ roles: user.roles }); - render(, { useRedux: true }); + render(, { useRedux: true, useQueryParams: true }); userEvent.hover(screen.getByText('Settings')); const label = await screen.findByText('Security'); expect(label).toBeInTheDocument(); @@ -344,7 +347,7 @@ test('should render the Settings dropdown child menu items', async () => { const { data: { settings }, } = mockedProps; - render(, { useRedux: true }); + render(, { useRedux: true, useQueryParams: true }); userEvent.hover(screen.getByText('Settings')); const listUsers = await screen.findByText('List Users'); expect(listUsers).toHaveAttribute('href', settings[0].childs[0].url); @@ -352,13 +355,13 @@ test('should render the Settings dropdown child menu items', async () => { test('should render the plus menu (+) when user is not anonymous', () => { useSelectorMock.mockReturnValue({ roles: user.roles }); - render(, { useRedux: true }); + render(, { useRedux: true, useQueryParams: true }); expect(screen.getByTestId('new-dropdown')).toBeInTheDocument(); }); test('should NOT render the plus menu (+) when user is anonymous', () => { useSelectorMock.mockReturnValue({ roles: user.roles }); - render(, { useRedux: true }); + render(, { useRedux: true, useQueryParams: true }); expect(screen.queryByTestId('new-dropdown')).not.toBeInTheDocument(); }); @@ -370,7 +373,7 @@ test('should render the user actions when user is not anonymous', async () => { }, } = mockedProps; - render(, { useRedux: true }); + render(, { useRedux: true, useQueryParams: true }); userEvent.hover(screen.getByText('Settings')); const user = await screen.findByText('User'); expect(user).toBeInTheDocument(); @@ -384,7 +387,7 @@ test('should render the user actions when user is not anonymous', async () => { test('should NOT render the user actions when user is anonymous', () => { useSelectorMock.mockReturnValue({ roles: user.roles }); - render(, { useRedux: true }); + render(, { useRedux: true, useQueryParams: true }); expect(screen.queryByText('User')).not.toBeInTheDocument(); }); @@ -396,7 +399,7 @@ test('should render the Profile link when available', async () => { }, } = mockedProps; - render(, { useRedux: true }); + render(, { useRedux: true, useQueryParams: true }); userEvent.hover(screen.getByText('Settings')); const profile = await screen.findByText('Profile'); @@ -411,7 +414,7 @@ test('should render the About section and version_string, sha or build_number wh }, } = mockedProps; - render(, { useRedux: true }); + render(, { useRedux: true, useQueryParams: true }); userEvent.hover(screen.getByText('Settings')); const about = await screen.findByText('About'); const version = await screen.findByText(`Version: ${version_string}`); @@ -430,7 +433,7 @@ test('should render the Documentation link when available', async () => { navbar_right: { documentation_url }, }, } = mockedProps; - render(, { useRedux: true }); + render(, { useRedux: true, useQueryParams: true }); userEvent.hover(screen.getByText('Settings')); const doc = await screen.findByTitle('Documentation'); expect(doc).toHaveAttribute('href', documentation_url); @@ -444,7 +447,7 @@ test('should render the Bug Report link when available', async () => { }, } = mockedProps; - render(, { useRedux: true }); + render(, { useRedux: true, useQueryParams: true }); const bugReport = await screen.findByTitle('Report a bug'); expect(bugReport).toHaveAttribute('href', bug_report_url); }); @@ -457,19 +460,19 @@ test('should render the Login link when user is anonymous', () => { }, } = mockedProps; - render(, { useRedux: true }); + render(, { useRedux: true, useQueryParams: true }); const login = screen.getByText('Login'); expect(login).toHaveAttribute('href', user_login_url); }); test('should render the Language Picker', () => { useSelectorMock.mockReturnValue({ roles: user.roles }); - render(, { useRedux: true }); + render(, { useRedux: true, useQueryParams: true }); expect(screen.getByLabelText('Languages')).toBeInTheDocument(); }); test('should hide create button without proper roles', () => { useSelectorMock.mockReturnValue({ roles: [] }); - render(, { useRedux: true }); + render(, { useRedux: true, useQueryParams: true }); expect(screen.queryByTestId('new-dropdown')).not.toBeInTheDocument(); }); diff --git a/superset-frontend/src/views/components/MenuRight.tsx b/superset-frontend/src/views/components/MenuRight.tsx index 4c34b883491c4..61bc6de0d6926 100644 --- a/superset-frontend/src/views/components/MenuRight.tsx +++ b/superset-frontend/src/views/components/MenuRight.tsx @@ -20,6 +20,8 @@ import React, { Fragment, useState, useEffect } from 'react'; import rison from 'rison'; import { useSelector } from 'react-redux'; import { Link } from 'react-router-dom'; +import { useQueryParams, BooleanParam } from 'use-query-params'; + import { t, styled, @@ -94,6 +96,10 @@ const RightMenu = ({ state => state.dashboardInfo?.id, ); + const [, setQuery] = useQueryParams({ + databaseAdded: BooleanParam, + }); + const { roles } = user; const { CSV_EXTENSIONS, @@ -250,6 +256,8 @@ const RightMenu = ({ return null; }; + const handleDatabaseAdd = () => setQuery({ databaseAdded: true }); + return ( {canDatabase && ( @@ -257,6 +265,7 @@ const RightMenu = ({ onHide={handleOnHideModal} show={showModal} dbEngine={engine} + onDatabaseAdd={handleDatabaseAdd} /> )} Date: Mon, 13 Jun 2022 07:44:34 -0300 Subject: [PATCH 20/71] chore: Removes no-use-before-define warnings (#20298) --- .../src/switchboard.ts | 114 +++++++------- .../components/AdvancedFrame.tsx | 12 +- .../src/middleware/asyncEvent.ts | 148 +++++++++--------- .../src/utils/localStorageHelpers.ts | 28 ++-- .../views/CRUD/data/dataset/DatasetList.tsx | 16 +- .../src/views/CRUD/welcome/ChartTable.tsx | 30 ++-- .../src/views/CRUD/welcome/DashboardTable.tsx | 74 ++++----- 7 files changed, 211 insertions(+), 211 deletions(-) diff --git a/superset-frontend/packages/superset-ui-switchboard/src/switchboard.ts b/superset-frontend/packages/superset-ui-switchboard/src/switchboard.ts index b65ca13586377..f12c9b6482c3d 100644 --- a/superset-frontend/packages/superset-ui-switchboard/src/switchboard.ts +++ b/superset-frontend/packages/superset-ui-switchboard/src/switchboard.ts @@ -23,6 +23,63 @@ export type Params = { debug?: boolean; }; +// Each message we send on the channel specifies an action we want the other side to cooperate with. +enum Actions { + GET = 'get', + REPLY = 'reply', + EMIT = 'emit', + ERROR = 'error', +} + +type Method = (args: A) => R | Promise; + +// helper types/functions for making sure wires don't get crossed + +interface Message { + switchboardAction: Actions; +} + +interface GetMessage extends Message { + switchboardAction: Actions.GET; + method: string; + messageId: string; + args: T; +} + +function isGet(message: Message): message is GetMessage { + return message.switchboardAction === Actions.GET; +} + +interface ReplyMessage extends Message { + switchboardAction: Actions.REPLY; + messageId: string; + result: T; +} + +function isReply(message: Message): message is ReplyMessage { + return message.switchboardAction === Actions.REPLY; +} + +interface EmitMessage extends Message { + switchboardAction: Actions.EMIT; + method: string; + args: T; +} + +function isEmit(message: Message): message is EmitMessage { + return message.switchboardAction === Actions.EMIT; +} + +interface ErrorMessage extends Message { + switchboardAction: Actions.ERROR; + messageId: string; + error: string; +} + +function isError(message: Message): message is ErrorMessage { + return message.switchboardAction === Actions.ERROR; +} + /** * A utility for communications between an iframe and its parent, used by the Superset embedded SDK. * This builds useful patterns on top of the basic functionality offered by MessageChannel. @@ -185,60 +242,3 @@ export class Switchboard { return `m_${this.name}_${this.incrementor++}`; } } - -type Method = (args: A) => R | Promise; - -// Each message we send on the channel specifies an action we want the other side to cooperate with. -enum Actions { - GET = 'get', - REPLY = 'reply', - EMIT = 'emit', - ERROR = 'error', -} - -// helper types/functions for making sure wires don't get crossed - -interface Message { - switchboardAction: Actions; -} - -interface GetMessage extends Message { - switchboardAction: Actions.GET; - method: string; - messageId: string; - args: T; -} - -function isGet(message: Message): message is GetMessage { - return message.switchboardAction === Actions.GET; -} - -interface ReplyMessage extends Message { - switchboardAction: Actions.REPLY; - messageId: string; - result: T; -} - -function isReply(message: Message): message is ReplyMessage { - return message.switchboardAction === Actions.REPLY; -} - -interface EmitMessage extends Message { - switchboardAction: Actions.EMIT; - method: string; - args: T; -} - -function isEmit(message: Message): message is EmitMessage { - return message.switchboardAction === Actions.EMIT; -} - -interface ErrorMessage extends Message { - switchboardAction: Actions.ERROR; - messageId: string; - error: string; -} - -function isError(message: Message): message is ErrorMessage { - return message.switchboardAction === Actions.ERROR; -} diff --git a/superset-frontend/src/explore/components/controls/DateFilterControl/components/AdvancedFrame.tsx b/superset-frontend/src/explore/components/controls/DateFilterControl/components/AdvancedFrame.tsx index a727a3f261aa2..f865b703a78ab 100644 --- a/superset-frontend/src/explore/components/controls/DateFilterControl/components/AdvancedFrame.tsx +++ b/superset-frontend/src/explore/components/controls/DateFilterControl/components/AdvancedFrame.tsx @@ -25,12 +25,6 @@ import { FrameComponentProps } from 'src/explore/components/controls/DateFilterC import DateFunctionTooltip from './DateFunctionTooltip'; export function AdvancedFrame(props: FrameComponentProps) { - const advancedRange = getAdvancedRange(props.value || ''); - const [since, until] = advancedRange.split(SEPARATOR); - if (advancedRange !== props.value) { - props.onChange(getAdvancedRange(props.value || '')); - } - function getAdvancedRange(value: string): string { if (value.includes(SEPARATOR)) { return value; @@ -44,6 +38,12 @@ export function AdvancedFrame(props: FrameComponentProps) { return SEPARATOR; } + const advancedRange = getAdvancedRange(props.value || ''); + const [since, until] = advancedRange.split(SEPARATOR); + if (advancedRange !== props.value) { + props.onChange(getAdvancedRange(props.value || '')); + } + function onChange(control: 'since' | 'until', value: string) { if (control === 'since') { props.onChange(`${value}${SEPARATOR}${until}`); diff --git a/superset-frontend/src/middleware/asyncEvent.ts b/superset-frontend/src/middleware/asyncEvent.ts index 638f324a4da25..9d252f99cc4d4 100644 --- a/superset-frontend/src/middleware/asyncEvent.ts +++ b/superset-frontend/src/middleware/asyncEvent.ts @@ -67,46 +67,6 @@ let listenersByJobId: Record; let retriesByJobId: Record; let lastReceivedEventId: string | null | undefined; -export const init = (appConfig?: AppConfig) => { - if (!isFeatureEnabled(FeatureFlag.GLOBAL_ASYNC_QUERIES)) return; - if (pollingTimeoutId) clearTimeout(pollingTimeoutId); - - listenersByJobId = {}; - retriesByJobId = {}; - lastReceivedEventId = null; - - if (appConfig) { - config = appConfig; - } else { - // load bootstrap data from DOM - const appContainer = document.getElementById('app'); - if (appContainer) { - const bootstrapData = JSON.parse( - appContainer?.getAttribute('data-bootstrap') || '{}', - ); - config = bootstrapData?.common?.conf; - } else { - config = {}; - logging.warn('asyncEvent: app config data not found'); - } - } - transport = config.GLOBAL_ASYNC_QUERIES_TRANSPORT || TRANSPORT_POLLING; - pollingDelayMs = config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY || 500; - - try { - lastReceivedEventId = localStorage.getItem(LOCALSTORAGE_KEY); - } catch (err) { - logging.warn('Failed to fetch last event Id from localStorage'); - } - - if (transport === TRANSPORT_POLLING) { - loadEventsFromApi(); - } - if (transport === TRANSPORT_WS) { - wsConnect(); - } -}; - const addListener = (id: string, fn: any) => { listenersByJobId[id] = fn; }; @@ -116,6 +76,24 @@ const removeListener = (id: string) => { delete listenersByJobId[id]; }; +const fetchCachedData = async ( + asyncEvent: AsyncEvent, +): Promise => { + let status = 'success'; + let data; + try { + const { json } = await SupersetClient.get({ + endpoint: String(asyncEvent.result_url), + }); + data = 'result' in json ? json.result : json; + } catch (response) { + status = 'error'; + data = await getClientErrorObject(response); + } + + return { status, data }; +}; + export const waitForAsyncData = async (asyncResponse: AsyncEvent) => new Promise((resolve, reject) => { const jobId = asyncResponse.job_id; @@ -153,24 +131,6 @@ const fetchEvents = makeApi< endpoint: POLLING_URL, }); -const fetchCachedData = async ( - asyncEvent: AsyncEvent, -): Promise => { - let status = 'success'; - let data; - try { - const { json } = await SupersetClient.get({ - endpoint: String(asyncEvent.result_url), - }); - data = 'result' in json ? json.result : json; - } catch (response) { - status = 'error'; - data = await getClientErrorObject(response); - } - - return { status, data }; -}; - const setLastId = (asyncEvent: AsyncEvent) => { lastReceivedEventId = asyncEvent.id; try { @@ -180,22 +140,6 @@ const setLastId = (asyncEvent: AsyncEvent) => { } }; -const loadEventsFromApi = async () => { - const eventArgs = lastReceivedEventId ? { last_id: lastReceivedEventId } : {}; - if (Object.keys(listenersByJobId).length) { - try { - const { result: events } = await fetchEvents(eventArgs); - if (events && events.length) await processEvents(events); - } catch (err) { - logging.warn(err); - } - } - - if (transport === TRANSPORT_POLLING) { - pollingTimeoutId = window.setTimeout(loadEventsFromApi, pollingDelayMs); - } -}; - export const processEvents = async (events: AsyncEvent[]) => { events.forEach((asyncEvent: AsyncEvent) => { const jobId = asyncEvent.job_id; @@ -222,6 +166,22 @@ export const processEvents = async (events: AsyncEvent[]) => { }); }; +const loadEventsFromApi = async () => { + const eventArgs = lastReceivedEventId ? { last_id: lastReceivedEventId } : {}; + if (Object.keys(listenersByJobId).length) { + try { + const { result: events } = await fetchEvents(eventArgs); + if (events && events.length) await processEvents(events); + } catch (err) { + logging.warn(err); + } + } + + if (transport === TRANSPORT_POLLING) { + pollingTimeoutId = window.setTimeout(loadEventsFromApi, pollingDelayMs); + } +}; + const wsConnectMaxRetries = 6; const wsConnectErrorDelay = 2500; let wsConnectRetries = 0; @@ -267,4 +227,44 @@ const wsConnect = (): void => { }); }; +export const init = (appConfig?: AppConfig) => { + if (!isFeatureEnabled(FeatureFlag.GLOBAL_ASYNC_QUERIES)) return; + if (pollingTimeoutId) clearTimeout(pollingTimeoutId); + + listenersByJobId = {}; + retriesByJobId = {}; + lastReceivedEventId = null; + + if (appConfig) { + config = appConfig; + } else { + // load bootstrap data from DOM + const appContainer = document.getElementById('app'); + if (appContainer) { + const bootstrapData = JSON.parse( + appContainer?.getAttribute('data-bootstrap') || '{}', + ); + config = bootstrapData?.common?.conf; + } else { + config = {}; + logging.warn('asyncEvent: app config data not found'); + } + } + transport = config.GLOBAL_ASYNC_QUERIES_TRANSPORT || TRANSPORT_POLLING; + pollingDelayMs = config.GLOBAL_ASYNC_QUERIES_POLLING_DELAY || 500; + + try { + lastReceivedEventId = localStorage.getItem(LOCALSTORAGE_KEY); + } catch (err) { + logging.warn('Failed to fetch last event Id from localStorage'); + } + + if (transport === TRANSPORT_POLLING) { + loadEventsFromApi(); + } + if (transport === TRANSPORT_WS) { + wsConnect(); + } +}; + init(); diff --git a/superset-frontend/src/utils/localStorageHelpers.ts b/superset-frontend/src/utils/localStorageHelpers.ts index 49044a162c6af..482f413372b69 100644 --- a/superset-frontend/src/utils/localStorageHelpers.ts +++ b/superset-frontend/src/utils/localStorageHelpers.ts @@ -66,20 +66,6 @@ export type LocalStorageValues = { explore__data_table_original_formatted_time_columns: Record; }; -export function getItem( - key: K, - defaultValue: LocalStorageValues[K], -): LocalStorageValues[K] { - return dangerouslyGetItemDoNotUse(key, defaultValue); -} - -export function setItem( - key: K, - value: LocalStorageValues[K], -): void { - dangerouslySetItemDoNotUse(key, value); -} - /* * This function should not be used directly, as it doesn't provide any type safety or any * guarantees that the globally namespaced localstorage key is correct. @@ -116,3 +102,17 @@ export function dangerouslySetItemDoNotUse(key: string, value: any): void { // Catch in case localStorage is unavailable } } + +export function getItem( + key: K, + defaultValue: LocalStorageValues[K], +): LocalStorageValues[K] { + return dangerouslyGetItemDoNotUse(key, defaultValue); +} + +export function setItem( + key: K, + value: LocalStorageValues[K], +): void { + dangerouslySetItemDoNotUse(key, value); +} diff --git a/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx b/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx index a56a69b346c11..7d6373b935d35 100644 --- a/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx +++ b/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx @@ -230,6 +230,14 @@ const DatasetList: FunctionComponent = ({ ), ); + const handleBulkDatasetExport = (datasetsToExport: Dataset[]) => { + const ids = datasetsToExport.map(({ id }) => id); + handleResourceExport('dataset', ids, () => { + setPreparingExport(false); + }); + setPreparingExport(true); + }; + const columns = useMemo( () => [ { @@ -617,14 +625,6 @@ const DatasetList: FunctionComponent = ({ ); }; - const handleBulkDatasetExport = (datasetsToExport: Dataset[]) => { - const ids = datasetsToExport.map(({ id }) => id); - handleResourceExport('dataset', ids, () => { - setPreparingExport(false); - }); - setPreparingExport(true); - }; - return ( <> diff --git a/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx b/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx index a035045318a21..41c25033df61c 100644 --- a/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx +++ b/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx @@ -106,21 +106,6 @@ function ChartTable({ const [preparingExport, setPreparingExport] = useState(false); const [loaded, setLoaded] = useState(false); - useEffect(() => { - if (loaded || chartFilter === 'Favorite') { - getData(chartFilter); - } - setLoaded(true); - }, [chartFilter]); - - const handleBulkChartExport = (chartsToExport: Chart[]) => { - const ids = chartsToExport.map(({ id }) => id); - handleResourceExport('chart', ids, () => { - setPreparingExport(false); - }); - setPreparingExport(true); - }; - const getFilters = (filterName: string) => { const filters = []; @@ -159,6 +144,21 @@ function ChartTable({ filters: getFilters(filter), }); + useEffect(() => { + if (loaded || chartFilter === 'Favorite') { + getData(chartFilter); + } + setLoaded(true); + }, [chartFilter]); + + const handleBulkChartExport = (chartsToExport: Chart[]) => { + const ids = chartsToExport.map(({ id }) => id); + handleResourceExport('chart', ids, () => { + setPreparingExport(false); + }); + setPreparingExport(true); + }; + const menuTabs = [ { name: 'Favorite', diff --git a/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx b/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx index 4078e23c2dec8..a5078465fc6f8 100644 --- a/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx +++ b/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx @@ -96,6 +96,43 @@ function DashboardTable({ const [preparingExport, setPreparingExport] = useState(false); const [loaded, setLoaded] = useState(false); + const getFilters = (filterName: string) => { + const filters = []; + if (filterName === 'Mine') { + filters.push({ + id: 'owners', + operator: 'rel_m_m', + value: `${user?.userId}`, + }); + } else if (filterName === 'Favorite') { + filters.push({ + id: 'id', + operator: 'dashboard_is_favorite', + value: true, + }); + } else if (filterName === 'Examples') { + filters.push({ + id: 'created_by', + operator: 'rel_o_m', + value: 0, + }); + } + return filters; + }; + + const getData = (filter: string) => + fetchData({ + pageIndex: 0, + pageSize: PAGE_SIZE, + sortBy: [ + { + id: 'changed_on_delta_humanized', + desc: true, + }, + ], + filters: getFilters(filter), + }); + useEffect(() => { if (loaded || dashboardFilter === 'Favorite') { getData(dashboardFilter); @@ -132,30 +169,6 @@ function DashboardTable({ ), ); - const getFilters = (filterName: string) => { - const filters = []; - if (filterName === 'Mine') { - filters.push({ - id: 'owners', - operator: 'rel_m_m', - value: `${user?.userId}`, - }); - } else if (filterName === 'Favorite') { - filters.push({ - id: 'id', - operator: 'dashboard_is_favorite', - value: true, - }); - } else if (filterName === 'Examples') { - filters.push({ - id: 'created_by', - operator: 'rel_o_m', - value: 0, - }); - } - return filters; - }; - const menuTabs = [ { name: 'Favorite', @@ -192,19 +205,6 @@ function DashboardTable({ }); } - const getData = (filter: string) => - fetchData({ - pageIndex: 0, - pageSize: PAGE_SIZE, - sortBy: [ - { - id: 'changed_on_delta_humanized', - desc: true, - }, - ], - filters: getFilters(filter), - }); - if (loading) return ; return ( <> From c6b1523db548f9eb325439dedef6c78fa6aa9e15 Mon Sep 17 00:00:00 2001 From: Elizabeth Thompson Date: Mon, 13 Jun 2022 11:48:22 -0700 Subject: [PATCH 21/71] add breaking change information about form_data datasource_type (#20321) --- UPDATING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/UPDATING.md b/UPDATING.md index 2e000eb885661..0573224b7107f 100644 --- a/UPDATING.md +++ b/UPDATING.md @@ -31,10 +31,10 @@ assists people when migrating to a new version. ### Breaking Changes -- [19770](https://github.com/apache/superset/pull/19770): As per SIPs 11 and 68, the native NoSQL Druid connector is deprecated and has been removed. Druid is still supported through SQLAlchemy via pydruid. The config keys `DRUID_IS_ACTIVE` and `DRUID_METADATA_LINKS_ENABLED` have also been removed. +- [19981](https://github.com/apache/superset/pull/19981): Per [SIP-81](https://github.com/apache/superset/issues/19953) the /explore/form_data api now requires a `datasource_type` in addition to a `datasource_id` for POST and PUT requests +- [19770](https://github.com/apache/superset/pull/19770): Per [SIP-11](https://github.com/apache/superset/issues/6032) and [SIP-68](https://github.com/apache/superset/issues/14909), the native NoSQL Druid connector is deprecated and has been removed. Druid is still supported through SQLAlchemy via pydruid. The config keys `DRUID_IS_ACTIVE` and `DRUID_METADATA_LINKS_ENABLED` have also been removed. - [19274](https://github.com/apache/superset/pull/19274): The `PUBLIC_ROLE_LIKE_GAMMA` config key has been removed, set `PUBLIC_ROLE_LIKE = "Gamma"` to have the same functionality. - [19273](https://github.com/apache/superset/pull/19273): The `SUPERSET_CELERY_WORKERS` and `SUPERSET_WORKERS` config keys has been removed. Configure Celery directly using `CELERY_CONFIG` on Superset. -- [19262](https://github.com/apache/superset/pull/19262): Per [SIP-11](https://github.com/apache/superset/issues/6032) and [SIP-68](https://github.com/apache/superset/issues/14909) the native NoSQL Druid connector is deprecated and will no longer be supported. Druid SQL is still [supported](https://superset.apache.org/docs/databases/druid). - [19231](https://github.com/apache/superset/pull/19231): The `ENABLE_REACT_CRUD_VIEWS` feature flag has been removed (premantly enabled). Any deployments which had set this flag to false will need to verify that the React views support their use case. - [19230](https://github.com/apache/superset/pull/19230): The `ROW_LEVEL_SECURITY` feature flag has been removed (permantly enabled). Any deployments which had set this flag to false will need to verify that the presence of the Row Level Security feature does not interfere with their use case. - [19168](https://github.com/apache/superset/pull/19168): Celery upgrade to 5.X resulted in breaking changes to its command line invocation. Please follow [these](https://docs.celeryq.dev/en/stable/whatsnew-5.2.html#step-1-adjust-your-command-line-invocation) instructions for adjustments. Also consider migrating you Celery config per [here](https://docs.celeryq.dev/en/stable/userguide/configuration.html#conf-old-settings-map). From ee06d3d113cd7d61e6e5e6162c59a66d775e7779 Mon Sep 17 00:00:00 2001 From: Smart-Codi Date: Mon, 13 Jun 2022 21:29:18 +0100 Subject: [PATCH 22/71] Fix typo issue in Error handling message (#20365) --- superset/reports/commands/alert.py | 8 ++++---- superset/reports/commands/exceptions.py | 4 ++-- superset/translations/de/LC_MESSAGES/messages.json | 8 ++++---- superset/translations/de/LC_MESSAGES/messages.po | 8 ++++---- superset/translations/en/LC_MESSAGES/messages.json | 8 ++++---- superset/translations/en/LC_MESSAGES/messages.po | 8 ++++---- superset/translations/es/LC_MESSAGES/messages.json | 8 ++++---- superset/translations/es/LC_MESSAGES/messages.po | 8 ++++---- superset/translations/fr/LC_MESSAGES/messages.json | 8 ++++---- superset/translations/fr/LC_MESSAGES/messages.po | 8 ++++---- superset/translations/it/LC_MESSAGES/messages.json | 8 ++++---- superset/translations/it/LC_MESSAGES/messages.po | 8 ++++---- superset/translations/ja/LC_MESSAGES/messages.json | 8 ++++---- superset/translations/ja/LC_MESSAGES/messages.po | 8 ++++---- superset/translations/ko/LC_MESSAGES/messages.json | 8 ++++---- superset/translations/ko/LC_MESSAGES/messages.po | 8 ++++---- superset/translations/messages.pot | 8 ++++---- superset/translations/nl/LC_MESSAGES/messages.json | 8 ++++---- superset/translations/nl/LC_MESSAGES/messages.po | 8 ++++---- superset/translations/pt/LC_MESSAGES/message.json | 8 ++++---- superset/translations/pt/LC_MESSAGES/message.po | 8 ++++---- superset/translations/pt_BR/LC_MESSAGES/messages.json | 8 ++++---- superset/translations/pt_BR/LC_MESSAGES/messages.po | 8 ++++---- superset/translations/ru/LC_MESSAGES/messages.json | 8 ++++---- superset/translations/ru/LC_MESSAGES/messages.po | 8 ++++---- superset/translations/sk/LC_MESSAGES/messages.json | 8 ++++---- superset/translations/sk/LC_MESSAGES/messages.po | 8 ++++---- superset/translations/sl/LC_MESSAGES/messages.json | 8 ++++---- superset/translations/sl/LC_MESSAGES/messages.po | 8 ++++---- superset/translations/zh/LC_MESSAGES/messages.json | 8 ++++---- superset/translations/zh/LC_MESSAGES/messages.po | 8 ++++---- 31 files changed, 122 insertions(+), 122 deletions(-) diff --git a/superset/reports/commands/alert.py b/superset/reports/commands/alert.py index 7a18996df3b3c..080fa3a3a763a 100644 --- a/superset/reports/commands/alert.py +++ b/superset/reports/commands/alert.py @@ -88,20 +88,20 @@ def _validate_not_null(self, rows: np.recarray) -> None: @staticmethod def _validate_result(rows: np.recarray) -> None: - # check if query return more then one row + # check if query return more than one row if len(rows) > 1: raise AlertQueryMultipleRowsError( message=_( - "Alert query returned more then one row. %s rows returned" + "Alert query returned more than one row. %s rows returned" % len(rows), ) ) - # check if query returned more then one column + # check if query returned more than one column if len(rows[0]) > 2: raise AlertQueryMultipleColumnsError( # len is subtracted by 1 to discard pandas index column _( - "Alert query returned more then one column. %s columns returned" + "Alert query returned more than one column. %s columns returned" % (len(rows[0]) - 1) ) ) diff --git a/superset/reports/commands/exceptions.py b/superset/reports/commands/exceptions.py index b78f99afe99db..db46503b93fd2 100644 --- a/superset/reports/commands/exceptions.py +++ b/superset/reports/commands/exceptions.py @@ -178,7 +178,7 @@ class ReportScheduleCreationMethodUniquenessValidationError(CommandException): class AlertQueryMultipleRowsError(CommandException): - message = _("Alert query returned more then one row.") + message = _("Alert query returned more than one row.") class AlertValidatorConfigError(CommandException): @@ -187,7 +187,7 @@ class AlertValidatorConfigError(CommandException): class AlertQueryMultipleColumnsError(CommandException): - message = _("Alert query returned more then one column.") + message = _("Alert query returned more than one column.") class AlertQueryInvalidTypeError(CommandException): diff --git a/superset/translations/de/LC_MESSAGES/messages.json b/superset/translations/de/LC_MESSAGES/messages.json index 98cd4e6947041..9abf6b6d8a7e9 100644 --- a/superset/translations/de/LC_MESSAGES/messages.json +++ b/superset/translations/de/LC_MESSAGES/messages.json @@ -279,16 +279,16 @@ "Alert query returned a non-number value.": [ "Die Alarm-Abfrage hat einen nicht-numerischen Wert zurückgegeben." ], - "Alert query returned more then one column.": [ + "Alert query returned more than one column.": [ "Die Alarm-Abfrage hat mehr als eine Spalte zurückgegeben." ], - "Alert query returned more then one column. %s columns returned": [ + "Alert query returned more than one column. %s columns returned": [ "Die Alarmabfrage hat mehr als eine Spalte zurückgegeben. %s Spalten zurückgegegeben" ], - "Alert query returned more then one row.": [ + "Alert query returned more than one row.": [ "Die Alarm-Abfrage hat mehr als eine Zeile zurückgegeben." ], - "Alert query returned more then one row. %s rows returned": [ + "Alert query returned more than one row. %s rows returned": [ "Die Alarmabfrage hat mehr als eine Zeile zurückgegeben. %s zurückgegebene Zeilen" ], "Alert running": ["Alarm wird ausgeführt"], diff --git a/superset/translations/de/LC_MESSAGES/messages.po b/superset/translations/de/LC_MESSAGES/messages.po index 6feca31c40ca3..8726ceae19a23 100644 --- a/superset/translations/de/LC_MESSAGES/messages.po +++ b/superset/translations/de/LC_MESSAGES/messages.po @@ -1028,23 +1028,23 @@ msgid "Alert query returned a non-number value." msgstr "Die Alarm-Abfrage hat einen nicht-numerischen Wert zurückgegeben." #: superset/reports/commands/exceptions.py:186 -msgid "Alert query returned more then one column." +msgid "Alert query returned more than one column." msgstr "Die Alarm-Abfrage hat mehr als eine Spalte zurückgegeben." #: superset/reports/commands/alert.py:105 #, python-format -msgid "Alert query returned more then one column. %s columns returned" +msgid "Alert query returned more than one column. %s columns returned" msgstr "" "Die Alarmabfrage hat mehr als eine Spalte zurückgegeben. %s Spalten " "zurückgegegeben" #: superset/reports/commands/exceptions.py:177 -msgid "Alert query returned more then one row." +msgid "Alert query returned more than one row." msgstr "Die Alarm-Abfrage hat mehr als eine Zeile zurückgegeben." #: superset/reports/commands/alert.py:96 #, python-format -msgid "Alert query returned more then one row. %s rows returned" +msgid "Alert query returned more than one row. %s rows returned" msgstr "" "Die Alarmabfrage hat mehr als eine Zeile zurückgegeben. %s zurückgegebene" " Zeilen" diff --git a/superset/translations/en/LC_MESSAGES/messages.json b/superset/translations/en/LC_MESSAGES/messages.json index 912c9fde70681..f344b2c2ec4e4 100644 --- a/superset/translations/en/LC_MESSAGES/messages.json +++ b/superset/translations/en/LC_MESSAGES/messages.json @@ -455,8 +455,8 @@ "", "Deleted %(num)d report schedules" ], - "Alert query returned more then one row. %s rows returned": [""], - "Alert query returned more then one column. %s columns returned": [""], + "Alert query returned more than one row. %s rows returned": [""], + "Alert query returned more than one column. %s columns returned": [""], "Dashboard does not exist": [""], "Chart does not exist": [""], "Database is required for alerts": [""], @@ -473,9 +473,9 @@ "Report Schedule execution got an unexpected error.": [""], "Report Schedule is still working, refusing to re-compute.": [""], "Report Schedule reached a working timeout.": [""], - "Alert query returned more then one row.": [""], + "Alert query returned more than one row.": [""], "Alert validator config error.": [""], - "Alert query returned more then one column.": [""], + "Alert query returned more than one column.": [""], "Alert query returned a non-number value.": [""], "Alert found an error while executing a query.": [""], "Alert fired during grace period.": [""], diff --git a/superset/translations/en/LC_MESSAGES/messages.po b/superset/translations/en/LC_MESSAGES/messages.po index 332f0e50c4402..4fb9477b0ff36 100644 --- a/superset/translations/en/LC_MESSAGES/messages.po +++ b/superset/translations/en/LC_MESSAGES/messages.po @@ -919,21 +919,21 @@ msgid "Alert query returned a non-number value." msgstr "" #: superset/reports/commands/exceptions.py:186 -msgid "Alert query returned more then one column." +msgid "Alert query returned more than one column." msgstr "" #: superset/reports/commands/alert.py:105 #, python-format -msgid "Alert query returned more then one column. %s columns returned" +msgid "Alert query returned more than one column. %s columns returned" msgstr "" #: superset/reports/commands/exceptions.py:177 -msgid "Alert query returned more then one row." +msgid "Alert query returned more than one row." msgstr "" #: superset/reports/commands/alert.py:96 #, python-format -msgid "Alert query returned more then one row. %s rows returned" +msgid "Alert query returned more than one row. %s rows returned" msgstr "" #: superset-frontend/src/views/CRUD/alert/components/AlertStatusIcon.tsx:75 diff --git a/superset/translations/es/LC_MESSAGES/messages.json b/superset/translations/es/LC_MESSAGES/messages.json index a7f2a4738b957..71487fc75089d 100644 --- a/superset/translations/es/LC_MESSAGES/messages.json +++ b/superset/translations/es/LC_MESSAGES/messages.json @@ -688,10 +688,10 @@ "", "Deleted %(num)d report schedules" ], - "Alert query returned more then one row. %s rows returned": [ + "Alert query returned more than one row. %s rows returned": [ "La consulta de alerta devolvió más de una fila. %s filas devueltas" ], - "Alert query returned more then one column. %s columns returned": [ + "Alert query returned more than one column. %s columns returned": [ "La consulta de alerta devolvió más de una columna. %s columnas devueltas" ], "Dashboard does not exist": ["El dashboard no existe"], @@ -734,13 +734,13 @@ "Report Schedule reached a working timeout.": [ "El informe programado alcanzó el tiempo de espera máximo." ], - "Alert query returned more then one row.": [ + "Alert query returned more than one row.": [ "La consulta de alerta devolvió más de una fila." ], "Alert validator config error.": [ "Error de configuración del validador de alertas." ], - "Alert query returned more then one column.": [ + "Alert query returned more than one column.": [ "La consulta de alerta devolvió más de una columna." ], "Alert query returned a non-number value.": [ diff --git a/superset/translations/es/LC_MESSAGES/messages.po b/superset/translations/es/LC_MESSAGES/messages.po index 2270f0db52d3c..54845502ec821 100644 --- a/superset/translations/es/LC_MESSAGES/messages.po +++ b/superset/translations/es/LC_MESSAGES/messages.po @@ -967,21 +967,21 @@ msgid "Alert query returned a non-number value." msgstr "La consulta de alerta devolvió un valor no numérico." #: superset/reports/commands/exceptions.py:186 -msgid "Alert query returned more then one column." +msgid "Alert query returned more than one column." msgstr "La consulta de alerta devolvió más de una columna." #: superset/reports/commands/alert.py:105 #, python-format -msgid "Alert query returned more then one column. %s columns returned" +msgid "Alert query returned more than one column. %s columns returned" msgstr "La consulta de alerta devolvió más de una columna. %s columnas devueltas" #: superset/reports/commands/exceptions.py:177 -msgid "Alert query returned more then one row." +msgid "Alert query returned more than one row." msgstr "La consulta de alerta devolvió más de una fila." #: superset/reports/commands/alert.py:96 #, python-format -msgid "Alert query returned more then one row. %s rows returned" +msgid "Alert query returned more than one row. %s rows returned" msgstr "La consulta de alerta devolvió más de una fila. %s filas devueltas" #: superset-frontend/src/views/CRUD/alert/components/AlertStatusIcon.tsx:75 diff --git a/superset/translations/fr/LC_MESSAGES/messages.json b/superset/translations/fr/LC_MESSAGES/messages.json index ddcf3d746d88f..0f972745710b8 100644 --- a/superset/translations/fr/LC_MESSAGES/messages.json +++ b/superset/translations/fr/LC_MESSAGES/messages.json @@ -993,10 +993,10 @@ "%(num)d planifications de rapport supprimées" ], "Value must be greater than 0": ["La valeur doit être plus grande que 0"], - "Alert query returned more then one row. %s rows returned": [ + "Alert query returned more than one row. %s rows returned": [ "La requête a retourné plus d'une ligne. %s lignes retournées" ], - "Alert query returned more then one column. %s columns returned": [ + "Alert query returned more than one column. %s columns returned": [ "La requête a retourné plus d'une colonne. %s colonnes retournées" ], "Dashboard does not exist": ["Le tableau de bord n'existe pas"], @@ -1051,13 +1051,13 @@ "Report Schedule reached a working timeout.": [ "La planification de rapport a atteint un timeout d'exécution." ], - "Alert query returned more then one row.": [ + "Alert query returned more than one row.": [ "La requête a retourné plus d'une ligne." ], "Alert validator config error.": [ "Erreur de configuration du validateur." ], - "Alert query returned more then one column.": [ + "Alert query returned more than one column.": [ "La requête a retourné plus d'une colonne." ], "Alert query returned a non-number value.": [ diff --git a/superset/translations/fr/LC_MESSAGES/messages.po b/superset/translations/fr/LC_MESSAGES/messages.po index a54668777c51f..0661382664f2a 100644 --- a/superset/translations/fr/LC_MESSAGES/messages.po +++ b/superset/translations/fr/LC_MESSAGES/messages.po @@ -1006,21 +1006,21 @@ msgid "Alert query returned a non-number value." msgstr "La requête a retourné une valeur non numérique." #: superset/reports/commands/exceptions.py:186 -msgid "Alert query returned more then one column." +msgid "Alert query returned more than one column." msgstr "La requête a retourné plus d'une colonne." #: superset/reports/commands/alert.py:105 #, python-format -msgid "Alert query returned more then one column. %s columns returned" +msgid "Alert query returned more than one column. %s columns returned" msgstr "La requête a retourné plus d'une colonne. %s colonnes retournées" #: superset/reports/commands/exceptions.py:177 -msgid "Alert query returned more then one row." +msgid "Alert query returned more than one row." msgstr "La requête a retourné plus d'une ligne." #: superset/reports/commands/alert.py:96 #, python-format -msgid "Alert query returned more then one row. %s rows returned" +msgid "Alert query returned more than one row. %s rows returned" msgstr "La requête a retourné plus d'une ligne. %s lignes retournées" #: superset-frontend/src/views/CRUD/alert/components/AlertStatusIcon.tsx:75 diff --git a/superset/translations/it/LC_MESSAGES/messages.json b/superset/translations/it/LC_MESSAGES/messages.json index 3ae239dce553c..733224efbeb9e 100644 --- a/superset/translations/it/LC_MESSAGES/messages.json +++ b/superset/translations/it/LC_MESSAGES/messages.json @@ -501,8 +501,8 @@ ], "Saved query not found.": [""], "Deleted %(num)d report schedule": [""], - "Alert query returned more then one row. %s rows returned": [""], - "Alert query returned more then one column. %s columns returned": [""], + "Alert query returned more than one row. %s rows returned": [""], + "Alert query returned more than one column. %s columns returned": [""], "Dashboard does not exist": [""], "Chart does not exist": [""], "Database is required for alerts": [""], @@ -527,9 +527,9 @@ "Report Schedule execution got an unexpected error.": [""], "Report Schedule is still working, refusing to re-compute.": [""], "Report Schedule reached a working timeout.": [""], - "Alert query returned more then one row.": [""], + "Alert query returned more than one row.": [""], "Alert validator config error.": [""], - "Alert query returned more then one column.": [""], + "Alert query returned more than one column.": [""], "Alert query returned a non-number value.": [""], "Alert found an error while executing a query.": [""], "Alert fired during grace period.": [""], diff --git a/superset/translations/it/LC_MESSAGES/messages.po b/superset/translations/it/LC_MESSAGES/messages.po index 41353893bc09c..3873a4b5a8921 100644 --- a/superset/translations/it/LC_MESSAGES/messages.po +++ b/superset/translations/it/LC_MESSAGES/messages.po @@ -944,21 +944,21 @@ msgid "Alert query returned a non-number value." msgstr "" #: superset/reports/commands/exceptions.py:186 -msgid "Alert query returned more then one column." +msgid "Alert query returned more than one column." msgstr "" #: superset/reports/commands/alert.py:105 #, python-format -msgid "Alert query returned more then one column. %s columns returned" +msgid "Alert query returned more than one column. %s columns returned" msgstr "" #: superset/reports/commands/exceptions.py:177 -msgid "Alert query returned more then one row." +msgid "Alert query returned more than one row." msgstr "" #: superset/reports/commands/alert.py:96 #, python-format -msgid "Alert query returned more then one row. %s rows returned" +msgid "Alert query returned more than one row. %s rows returned" msgstr "" #: superset-frontend/src/views/CRUD/alert/components/AlertStatusIcon.tsx:75 diff --git a/superset/translations/ja/LC_MESSAGES/messages.json b/superset/translations/ja/LC_MESSAGES/messages.json index d0f2f9b93f46d..99e308cfa75ce 100644 --- a/superset/translations/ja/LC_MESSAGES/messages.json +++ b/superset/translations/ja/LC_MESSAGES/messages.json @@ -654,10 +654,10 @@ " %(num)d 件のレポートスケジュールを削除しました" ], "Value must be greater than 0": ["値は 0 より大きくする必要があります"], - "Alert query returned more then one row. %s rows returned": [ + "Alert query returned more than one row. %s rows returned": [ "アラートクエリが複数の行を返しました。 %s 行が返されました" ], - "Alert query returned more then one column. %s columns returned": [ + "Alert query returned more than one column. %s columns returned": [ "アラートクエリが複数の列を返しました。%s 列が返されました" ], "Dashboard does not exist": ["ダッシュボードが存在しません"], @@ -701,11 +701,11 @@ "Report Schedule reached a working timeout.": [ "レポートスケジュールが作業タイムアウトに達しました。" ], - "Alert query returned more then one row.": [ + "Alert query returned more than one row.": [ "アラートクエリが複数の行を返しました。" ], "Alert validator config error.": ["アラートバリデーター設定エラー。"], - "Alert query returned more then one column.": [ + "Alert query returned more than one column.": [ "アラートクエリが複数の列を返しました。" ], "Alert query returned a non-number value.": [ diff --git a/superset/translations/ja/LC_MESSAGES/messages.po b/superset/translations/ja/LC_MESSAGES/messages.po index f89d9f0b2b818..d34ad5518cd6f 100644 --- a/superset/translations/ja/LC_MESSAGES/messages.po +++ b/superset/translations/ja/LC_MESSAGES/messages.po @@ -938,21 +938,21 @@ msgid "Alert query returned a non-number value." msgstr "アラートクエリが数値以外の値を返しました。" #: superset/reports/commands/exceptions.py:186 -msgid "Alert query returned more then one column." +msgid "Alert query returned more than one column." msgstr "アラートクエリが複数の列を返しました。" #: superset/reports/commands/alert.py:105 #, python-format -msgid "Alert query returned more then one column. %s columns returned" +msgid "Alert query returned more than one column. %s columns returned" msgstr "アラートクエリが複数の列を返しました。%s 列が返されました" #: superset/reports/commands/exceptions.py:177 -msgid "Alert query returned more then one row." +msgid "Alert query returned more than one row." msgstr "アラートクエリが複数の行を返しました。" #: superset/reports/commands/alert.py:96 #, python-format -msgid "Alert query returned more then one row. %s rows returned" +msgid "Alert query returned more than one row. %s rows returned" msgstr "アラートクエリが複数の行を返しました。 %s 行が返されました" #: superset-frontend/src/views/CRUD/alert/components/AlertStatusIcon.tsx:75 diff --git a/superset/translations/ko/LC_MESSAGES/messages.json b/superset/translations/ko/LC_MESSAGES/messages.json index 3949e99ed673e..66cc9949a4cc4 100644 --- a/superset/translations/ko/LC_MESSAGES/messages.json +++ b/superset/translations/ko/LC_MESSAGES/messages.json @@ -543,8 +543,8 @@ "Saved query not found.": ["저장된 쿼리를 찾을 수 없습니다."], "Deleted %(num)d report schedule": [""], "Value must be greater than 0": ["값은 0보다 커야합니다"], - "Alert query returned more then one row. %s rows returned": [""], - "Alert query returned more then one column. %s columns returned": [""], + "Alert query returned more than one row. %s rows returned": [""], + "Alert query returned more than one column. %s columns returned": [""], "Dashboard does not exist": ["대시보드가 존재하지 않습니다"], "Chart does not exist": ["차트가 존재하지 않습니다"], "Database is required for alerts": [""], @@ -561,9 +561,9 @@ "Report Schedule execution got an unexpected error.": [""], "Report Schedule is still working, refusing to re-compute.": [""], "Report Schedule reached a working timeout.": [""], - "Alert query returned more then one row.": [""], + "Alert query returned more than one row.": [""], "Alert validator config error.": [""], - "Alert query returned more then one column.": [""], + "Alert query returned more than one column.": [""], "Alert query returned a non-number value.": [""], "Alert found an error while executing a query.": [""], "A timeout occurred while executing the query.": [""], diff --git a/superset/translations/ko/LC_MESSAGES/messages.po b/superset/translations/ko/LC_MESSAGES/messages.po index db408ecdcf3a6..5b4530e0399dc 100644 --- a/superset/translations/ko/LC_MESSAGES/messages.po +++ b/superset/translations/ko/LC_MESSAGES/messages.po @@ -938,21 +938,21 @@ msgid "Alert query returned a non-number value." msgstr "" #: superset/reports/commands/exceptions.py:186 -msgid "Alert query returned more then one column." +msgid "Alert query returned more than one column." msgstr "" #: superset/reports/commands/alert.py:105 #, python-format -msgid "Alert query returned more then one column. %s columns returned" +msgid "Alert query returned more than one column. %s columns returned" msgstr "" #: superset/reports/commands/exceptions.py:177 -msgid "Alert query returned more then one row." +msgid "Alert query returned more than one row." msgstr "" #: superset/reports/commands/alert.py:96 #, python-format -msgid "Alert query returned more then one row. %s rows returned" +msgid "Alert query returned more than one row. %s rows returned" msgstr "" #: superset-frontend/src/views/CRUD/alert/components/AlertStatusIcon.tsx:75 diff --git a/superset/translations/messages.pot b/superset/translations/messages.pot index 53f2e0506df02..2233ec416a6cd 100644 --- a/superset/translations/messages.pot +++ b/superset/translations/messages.pot @@ -925,21 +925,21 @@ msgid "Alert query returned a non-number value." msgstr "" #: superset/reports/commands/exceptions.py:186 -msgid "Alert query returned more then one column." +msgid "Alert query returned more than one column." msgstr "" #: superset/reports/commands/alert.py:105 #, python-format -msgid "Alert query returned more then one column. %s columns returned" +msgid "Alert query returned more than one column. %s columns returned" msgstr "" #: superset/reports/commands/exceptions.py:177 -msgid "Alert query returned more then one row." +msgid "Alert query returned more than one row." msgstr "" #: superset/reports/commands/alert.py:96 #, python-format -msgid "Alert query returned more then one row. %s rows returned" +msgid "Alert query returned more than one row. %s rows returned" msgstr "" #: superset-frontend/src/views/CRUD/alert/components/AlertStatusIcon.tsx:75 diff --git a/superset/translations/nl/LC_MESSAGES/messages.json b/superset/translations/nl/LC_MESSAGES/messages.json index 19ab66a085ebd..699eb84297d9d 100644 --- a/superset/translations/nl/LC_MESSAGES/messages.json +++ b/superset/translations/nl/LC_MESSAGES/messages.json @@ -909,10 +909,10 @@ "Verwijderde %(num)d rapport schema’s" ], "Value must be greater than 0": ["Waarde moet groter zijn dan 0"], - "Alert query returned more then one row. %s rows returned": [ + "Alert query returned more than one row. %s rows returned": [ "Alert query heeft meer dan één rij geretourneerd. %s rijen geretourneerd" ], - "Alert query returned more then one column. %s columns returned": [ + "Alert query returned more than one column. %s columns returned": [ "Alert query retourneerde meer dan één kolom. %s kolommen geretourneerd" ], "Dashboard does not exist": ["Het dashboard bestaat niet"], @@ -965,13 +965,13 @@ "Report Schedule reached a working timeout.": [ "Rapportage planning heeft een werk time-out bereikt." ], - "Alert query returned more then one row.": [ + "Alert query returned more than one row.": [ "Alert query retourneerde meer dan één rij." ], "Alert validator config error.": [ "Waarschuwing validator configuratiefout." ], - "Alert query returned more then one column.": [ + "Alert query returned more than one column.": [ "Alert query heeft meer dan één kolom geretourneerd." ], "Alert query returned a non-number value.": [ diff --git a/superset/translations/nl/LC_MESSAGES/messages.po b/superset/translations/nl/LC_MESSAGES/messages.po index 0f454a727a03f..5db5e5ae4e1d9 100644 --- a/superset/translations/nl/LC_MESSAGES/messages.po +++ b/superset/translations/nl/LC_MESSAGES/messages.po @@ -2415,13 +2415,13 @@ msgstr "Waarde moet groter zijn dan 0" #: superset/reports/commands/alert.py:96 #, python-format -msgid "Alert query returned more then one row. %s rows returned" +msgid "Alert query returned more than one row. %s rows returned" msgstr "" "Alert query heeft meer dan één rij geretourneerd. %s rijen geretourneerd" #: superset/reports/commands/alert.py:105 #, python-format -msgid "Alert query returned more then one column. %s columns returned" +msgid "Alert query returned more than one column. %s columns returned" msgstr "Alert query retourneerde meer dan één kolom. %s kolommen geretourneerd" #: superset/reports/commands/exceptions.py:44 @@ -2506,7 +2506,7 @@ msgid "Report Schedule reached a working timeout." msgstr "Rapportage planning heeft een werk time-out bereikt." #: superset/reports/commands/exceptions.py:177 -msgid "Alert query returned more then one row." +msgid "Alert query returned more than one row." msgstr "Alert query retourneerde meer dan één rij." #: superset/reports/commands/exceptions.py:182 @@ -2514,7 +2514,7 @@ msgid "Alert validator config error." msgstr "Waarschuwing validator configuratiefout." #: superset/reports/commands/exceptions.py:186 -msgid "Alert query returned more then one column." +msgid "Alert query returned more than one column." msgstr "Alert query heeft meer dan één kolom geretourneerd." #: superset/reports/commands/exceptions.py:190 diff --git a/superset/translations/pt/LC_MESSAGES/message.json b/superset/translations/pt/LC_MESSAGES/message.json index 9ea04398fbd89..7ff52195f553d 100644 --- a/superset/translations/pt/LC_MESSAGES/message.json +++ b/superset/translations/pt/LC_MESSAGES/message.json @@ -551,8 +551,8 @@ "", "Deleted %(num)d report schedules" ], - "Alert query returned more then one row. %s rows returned": [""], - "Alert query returned more then one column. %s columns returned": [""], + "Alert query returned more than one row. %s rows returned": [""], + "Alert query returned more than one column. %s columns returned": [""], "Dashboard does not exist": [""], "Chart does not exist": [""], "Database is required for alerts": [""], @@ -575,9 +575,9 @@ "Report Schedule execution got an unexpected error.": [""], "Report Schedule is still working, refusing to re-compute.": [""], "Report Schedule reached a working timeout.": [""], - "Alert query returned more then one row.": [""], + "Alert query returned more than one row.": [""], "Alert validator config error.": [""], - "Alert query returned more then one column.": [""], + "Alert query returned more than one column.": [""], "Alert query returned a non-number value.": [""], "Alert found an error while executing a query.": [""], "Alert fired during grace period.": [""], diff --git a/superset/translations/pt/LC_MESSAGES/message.po b/superset/translations/pt/LC_MESSAGES/message.po index 7fba415fee040..b944475c5591c 100644 --- a/superset/translations/pt/LC_MESSAGES/message.po +++ b/superset/translations/pt/LC_MESSAGES/message.po @@ -1788,12 +1788,12 @@ msgstr[1] "" #: superset/reports/commands/alert.py:74 #, python-format -msgid "Alert query returned more then one row. %s rows returned" +msgid "Alert query returned more than one row. %s rows returned" msgstr "" #: superset/reports/commands/alert.py:83 #, python-format -msgid "Alert query returned more then one column. %s columns returned" +msgid "Alert query returned more than one column. %s columns returned" msgstr "" #: superset/reports/commands/exceptions.py:44 @@ -1861,7 +1861,7 @@ msgid "Report Schedule reached a working timeout." msgstr "" #: superset/reports/commands/exceptions.py:138 -msgid "Alert query returned more then one row." +msgid "Alert query returned more than one row." msgstr "" #: superset/reports/commands/exceptions.py:143 @@ -1869,7 +1869,7 @@ msgid "Alert validator config error." msgstr "" #: superset/reports/commands/exceptions.py:147 -msgid "Alert query returned more then one column." +msgid "Alert query returned more than one column." msgstr "" #: superset/reports/commands/exceptions.py:151 diff --git a/superset/translations/pt_BR/LC_MESSAGES/messages.json b/superset/translations/pt_BR/LC_MESSAGES/messages.json index 370a179c4df22..253edc6ee4057 100644 --- a/superset/translations/pt_BR/LC_MESSAGES/messages.json +++ b/superset/translations/pt_BR/LC_MESSAGES/messages.json @@ -705,10 +705,10 @@ "%(num)d agendamento de relatório deletado", "%(num)d agendamentos de relatório deletados" ], - "Alert query returned more then one row. %s rows returned": [ + "Alert query returned more than one row. %s rows returned": [ "A consulta de alerta retornou mais de uma linha. %s linhas retornadas" ], - "Alert query returned more then one column. %s columns returned": [ + "Alert query returned more than one column. %s columns returned": [ "A consulta de alerta retornou mais de uma coluna. %s colunas retornadas" ], "Dashboard does not exist": ["Painel não existe"], @@ -753,13 +753,13 @@ "Report Schedule reached a working timeout.": [ "O agendamento de relatório atingiu o timeout de trabalho." ], - "Alert query returned more then one row.": [ + "Alert query returned more than one row.": [ "A consulta de alerta retornou mais de uma linha." ], "Alert validator config error.": [ "Erro de configuração do validador de alerta." ], - "Alert query returned more then one column.": [ + "Alert query returned more than one column.": [ "A consulta de alerta retornou mais de uma coluna." ], "Alert query returned a non-number value.": [ diff --git a/superset/translations/pt_BR/LC_MESSAGES/messages.po b/superset/translations/pt_BR/LC_MESSAGES/messages.po index 0092e89083ee2..2441b2c768325 100644 --- a/superset/translations/pt_BR/LC_MESSAGES/messages.po +++ b/superset/translations/pt_BR/LC_MESSAGES/messages.po @@ -1007,21 +1007,21 @@ msgid "Alert query returned a non-number value." msgstr "A consulta de alerta retornou um valor não numérico." #: superset/reports/commands/exceptions.py:186 -msgid "Alert query returned more then one column." +msgid "Alert query returned more than one column." msgstr "A consulta de alerta retornou mais de uma coluna." #: superset/reports/commands/alert.py:105 #, python-format -msgid "Alert query returned more then one column. %s columns returned" +msgid "Alert query returned more than one column. %s columns returned" msgstr "A consulta de alerta retornou mais de uma coluna. %s colunas retornadas" #: superset/reports/commands/exceptions.py:177 -msgid "Alert query returned more then one row." +msgid "Alert query returned more than one row." msgstr "A consulta de alerta retornou mais de uma linha." #: superset/reports/commands/alert.py:96 #, python-format -msgid "Alert query returned more then one row. %s rows returned" +msgid "Alert query returned more than one row. %s rows returned" msgstr "A consulta de alerta retornou mais de uma linha. %s linhas retornadas" #: superset-frontend/src/views/CRUD/alert/components/AlertStatusIcon.tsx:75 diff --git a/superset/translations/ru/LC_MESSAGES/messages.json b/superset/translations/ru/LC_MESSAGES/messages.json index c100ba7f66c3f..be3a0f1212e4d 100644 --- a/superset/translations/ru/LC_MESSAGES/messages.json +++ b/superset/translations/ru/LC_MESSAGES/messages.json @@ -648,10 +648,10 @@ ], "Saved query not found.": ["Сохранённый запрос не найден."], "Deleted %(num)d report schedule": ["Удалено %(num)d рассылок"], - "Alert query returned more then one row. %s rows returned": [ + "Alert query returned more than one row. %s rows returned": [ "Запрос от оповещения вернул больше одной строки. %s строк возвращено" ], - "Alert query returned more then one column. %s columns returned": [ + "Alert query returned more than one column. %s columns returned": [ "Запрос от оповещения вернул больше одного столбца. %s столбцов возвращено" ], "Dashboard does not exist": ["Дашборд не существует"], @@ -690,13 +690,13 @@ "Report Schedule reached a working timeout.": [ "Рассылка достигла тайм-аута в работе." ], - "Alert query returned more then one row.": [ + "Alert query returned more than one row.": [ "Запрос для оповещения вернул больше одной строки." ], "Alert validator config error.": [ "Неверная конфигурация широты и долготы." ], - "Alert query returned more then one column.": [ + "Alert query returned more than one column.": [ "Запрос для оповещения вернул больше одного столбца." ], "Alert query returned a non-number value.": [ diff --git a/superset/translations/ru/LC_MESSAGES/messages.po b/superset/translations/ru/LC_MESSAGES/messages.po index 9b79a3ea5708a..8cfde5eed55ce 100644 --- a/superset/translations/ru/LC_MESSAGES/messages.po +++ b/superset/translations/ru/LC_MESSAGES/messages.po @@ -984,21 +984,21 @@ msgid "Alert query returned a non-number value." msgstr "Запрос для оповещения вернул не число." #: superset/reports/commands/exceptions.py:186 -msgid "Alert query returned more then one column." +msgid "Alert query returned more than one column." msgstr "Запрос для оповещения вернул больше одного столбца." #: superset/reports/commands/alert.py:105 #, python-format -msgid "Alert query returned more then one column. %s columns returned" +msgid "Alert query returned more than one column. %s columns returned" msgstr "Запрос от оповещения вернул больше одного столбца. %s столбцов возвращено" #: superset/reports/commands/exceptions.py:177 -msgid "Alert query returned more then one row." +msgid "Alert query returned more than one row." msgstr "Запрос для оповещения вернул больше одной строки." #: superset/reports/commands/alert.py:96 #, python-format -msgid "Alert query returned more then one row. %s rows returned" +msgid "Alert query returned more than one row. %s rows returned" msgstr "Запрос от оповещения вернул больше одной строки. %s строк возвращено" #: superset-frontend/src/views/CRUD/alert/components/AlertStatusIcon.tsx:75 diff --git a/superset/translations/sk/LC_MESSAGES/messages.json b/superset/translations/sk/LC_MESSAGES/messages.json index ba2b175e20302..4418cabbf8530 100644 --- a/superset/translations/sk/LC_MESSAGES/messages.json +++ b/superset/translations/sk/LC_MESSAGES/messages.json @@ -582,8 +582,8 @@ "Deleted %(num)d report schedules" ], "Value must be greater than 0": [""], - "Alert query returned more then one row. %s rows returned": [""], - "Alert query returned more then one column. %s columns returned": [""], + "Alert query returned more than one row. %s rows returned": [""], + "Alert query returned more than one column. %s columns returned": [""], "Dashboard does not exist": [""], "Chart does not exist": [""], "Database is required for alerts": [""], @@ -601,9 +601,9 @@ "Report Schedule execution got an unexpected error.": [""], "Report Schedule is still working, refusing to re-compute.": [""], "Report Schedule reached a working timeout.": [""], - "Alert query returned more then one row.": [""], + "Alert query returned more than one row.": [""], "Alert validator config error.": [""], - "Alert query returned more then one column.": [""], + "Alert query returned more than one column.": [""], "Alert query returned a non-number value.": [""], "Alert found an error while executing a query.": [""], "A timeout occurred while executing the query.": [""], diff --git a/superset/translations/sk/LC_MESSAGES/messages.po b/superset/translations/sk/LC_MESSAGES/messages.po index cd8d69c0ebd40..6429587f83a59 100644 --- a/superset/translations/sk/LC_MESSAGES/messages.po +++ b/superset/translations/sk/LC_MESSAGES/messages.po @@ -919,21 +919,21 @@ msgid "Alert query returned a non-number value." msgstr "" #: superset/reports/commands/exceptions.py:186 -msgid "Alert query returned more then one column." +msgid "Alert query returned more than one column." msgstr "" #: superset/reports/commands/alert.py:105 #, python-format -msgid "Alert query returned more then one column. %s columns returned" +msgid "Alert query returned more than one column. %s columns returned" msgstr "" #: superset/reports/commands/exceptions.py:177 -msgid "Alert query returned more then one row." +msgid "Alert query returned more than one row." msgstr "" #: superset/reports/commands/alert.py:96 #, python-format -msgid "Alert query returned more then one row. %s rows returned" +msgid "Alert query returned more than one row. %s rows returned" msgstr "" #: superset-frontend/src/views/CRUD/alert/components/AlertStatusIcon.tsx:75 diff --git a/superset/translations/sl/LC_MESSAGES/messages.json b/superset/translations/sl/LC_MESSAGES/messages.json index c5ac73ad0c421..ab5e2067a7101 100644 --- a/superset/translations/sl/LC_MESSAGES/messages.json +++ b/superset/translations/sl/LC_MESSAGES/messages.json @@ -963,10 +963,10 @@ "Izbrisanih %(num)d urnikov poročanja" ], "Value must be greater than 0": ["Vrednost mora biti večja od 0"], - "Alert query returned more then one row. %s rows returned": [ + "Alert query returned more than one row. %s rows returned": [ "Opozorilna poizvedba je vrnila več kot eno vrstico. Število vrnjenih vrstic: %s" ], - "Alert query returned more then one column. %s columns returned": [ + "Alert query returned more than one column. %s columns returned": [ "Opozorilna poizvedba je vrnila več kot en stolpec. Število vrnjenih stolpcev: %s" ], "Dashboard does not exist": ["Nadzorna plošča ne obstaja"], @@ -1019,13 +1019,13 @@ "Report Schedule reached a working timeout.": [ "Urnik poročanja je dosegel mejo časa izvedbe." ], - "Alert query returned more then one row.": [ + "Alert query returned more than one row.": [ "Opozorilna poizvedba je vrnila več kot eno vrstico." ], "Alert validator config error.": [ "Napaka nastavitev potrjevalnika opozoril." ], - "Alert query returned more then one column.": [ + "Alert query returned more than one column.": [ "Opozorilna poizvedba je vrnila več kot en stolpec." ], "Alert query returned a non-number value.": [ diff --git a/superset/translations/sl/LC_MESSAGES/messages.po b/superset/translations/sl/LC_MESSAGES/messages.po index 5171d95052dd6..2c9ad7bfcb09c 100644 --- a/superset/translations/sl/LC_MESSAGES/messages.po +++ b/superset/translations/sl/LC_MESSAGES/messages.po @@ -1002,23 +1002,23 @@ msgid "Alert query returned a non-number value." msgstr "Opozorilna poizvedba je vrnila neštevilsko vrednost." #: superset/reports/commands/exceptions.py:186 -msgid "Alert query returned more then one column." +msgid "Alert query returned more than one column." msgstr "Opozorilna poizvedba je vrnila več kot en stolpec." #: superset/reports/commands/alert.py:105 #, python-format -msgid "Alert query returned more then one column. %s columns returned" +msgid "Alert query returned more than one column. %s columns returned" msgstr "" "Opozorilna poizvedba je vrnila več kot en stolpec. Število vrnjenih " "stolpcev: %s" #: superset/reports/commands/exceptions.py:177 -msgid "Alert query returned more then one row." +msgid "Alert query returned more than one row." msgstr "Opozorilna poizvedba je vrnila več kot eno vrstico." #: superset/reports/commands/alert.py:96 #, python-format -msgid "Alert query returned more then one row. %s rows returned" +msgid "Alert query returned more than one row. %s rows returned" msgstr "" "Opozorilna poizvedba je vrnila več kot eno vrstico. Število vrnjenih " "vrstic: %s" diff --git a/superset/translations/zh/LC_MESSAGES/messages.json b/superset/translations/zh/LC_MESSAGES/messages.json index 8d803b45ab49d..b7fd50528ee24 100644 --- a/superset/translations/zh/LC_MESSAGES/messages.json +++ b/superset/translations/zh/LC_MESSAGES/messages.json @@ -559,10 +559,10 @@ "Saved queries could not be deleted.": ["保存的查询无法被删除"], "Saved query not found.": ["保存的查询未找到"], "Deleted %(num)d report schedule": ["已经删除了 %(num)d 个报告时间表"], - "Alert query returned more then one row. %s rows returned": [ + "Alert query returned more than one row. %s rows returned": [ "警报查询返回了多行。%s 行被返回" ], - "Alert query returned more then one column. %s columns returned": [ + "Alert query returned more than one column. %s columns returned": [ "警报查询返回多个列。%s 列被返回" ], "Dashboard does not exist": ["看板不存在"], @@ -589,9 +589,9 @@ "报表计划仍在运行,拒绝重新计算。" ], "Report Schedule reached a working timeout.": ["报表计划已超时。"], - "Alert query returned more then one row.": ["警报查询返回了多行。"], + "Alert query returned more than one row.": ["警报查询返回了多行。"], "Alert validator config error.": ["错误的经纬度配置。"], - "Alert query returned more then one column.": ["警报查询返回多个列。"], + "Alert query returned more than one column.": ["警报查询返回多个列。"], "Alert query returned a non-number value.": ["警报查询返回非数字值。"], "Alert found an error while executing a query.": [ "警报在执行查询时发现错误。" diff --git a/superset/translations/zh/LC_MESSAGES/messages.po b/superset/translations/zh/LC_MESSAGES/messages.po index 1d6e5e25b5c3b..7b2570f7921ad 100644 --- a/superset/translations/zh/LC_MESSAGES/messages.po +++ b/superset/translations/zh/LC_MESSAGES/messages.po @@ -969,21 +969,21 @@ msgid "Alert query returned a non-number value." msgstr "警报查询返回非数字值。" #: superset/reports/commands/exceptions.py:186 -msgid "Alert query returned more then one column." +msgid "Alert query returned more than one column." msgstr "警报查询返回多个列。" #: superset/reports/commands/alert.py:105 #, python-format -msgid "Alert query returned more then one column. %s columns returned" +msgid "Alert query returned more than one column. %s columns returned" msgstr "警报查询返回多个列。%s 列被返回" #: superset/reports/commands/exceptions.py:177 -msgid "Alert query returned more then one row." +msgid "Alert query returned more than one row." msgstr "警报查询返回了多行。" #: superset/reports/commands/alert.py:96 #, python-format -msgid "Alert query returned more then one row. %s rows returned" +msgid "Alert query returned more than one row. %s rows returned" msgstr "警报查询返回了多行。%s 行被返回" #: superset-frontend/src/views/CRUD/alert/components/AlertStatusIcon.tsx:75 From e1532f63aca5dfd1386dd1cbd811f382ab9b7f98 Mon Sep 17 00:00:00 2001 From: Diego Medina Date: Mon, 13 Jun 2022 17:30:00 -0300 Subject: [PATCH 23/71] fix: query execution time is not fully displayed in bubble icon (#20364) --- superset-frontend/src/components/Timer/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/superset-frontend/src/components/Timer/index.tsx b/superset-frontend/src/components/Timer/index.tsx index b03e1a831f8e8..cfffc285717ba 100644 --- a/superset-frontend/src/components/Timer/index.tsx +++ b/superset-frontend/src/components/Timer/index.tsx @@ -31,7 +31,6 @@ export interface TimerProps { const TimerLabel = styled(Label)` text-align: left; - width: 91px; `; export default function Timer({ From 5a137820d0fd192fe8466e9448a59e327d13eeb5 Mon Sep 17 00:00:00 2001 From: Elizabeth Thompson Date: Mon, 13 Jun 2022 17:30:13 -0700 Subject: [PATCH 24/71] fix: catch some potential errors on dual write (#20351) * catch some potential errors on dual write * fix test for sqlite --- superset/connectors/sqla/models.py | 42 +++++++---- superset/connectors/sqla/utils.py | 11 ++- tests/integration_tests/datasets/api_tests.py | 6 ++ .../integration_tests/datasets/model_tests.py | 69 +++++++++++++++++++ .../integration_tests/fixtures/datasource.py | 52 +++++++++++++- 5 files changed, 163 insertions(+), 17 deletions(-) create mode 100644 tests/integration_tests/datasets/model_tests.py diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py index 60eff5e6304ab..3b404743317a0 100644 --- a/superset/connectors/sqla/models.py +++ b/superset/connectors/sqla/models.py @@ -66,6 +66,7 @@ ) from sqlalchemy.engine.base import Connection from sqlalchemy.orm import backref, Query, relationship, RelationshipProperty, Session +from sqlalchemy.orm.exc import NoResultFound from sqlalchemy.orm.mapper import Mapper from sqlalchemy.schema import UniqueConstraint from sqlalchemy.sql import column, ColumnElement, literal_column, table @@ -933,7 +934,8 @@ def mutate_query_from_config(self, sql: str) -> str: if sql_query_mutator: sql = sql_query_mutator( sql, - user_name=get_username(), # TODO(john-bodley): Deprecate in 3.0. + # TODO(john-bodley): Deprecate in 3.0. + user_name=get_username(), security_manager=security_manager, database=self.database, ) @@ -2115,7 +2117,7 @@ def get_sl_columns(self) -> List[NewColumn]: ] @staticmethod - def update_table( # pylint: disable=unused-argument + def update_column( # pylint: disable=unused-argument mapper: Mapper, connection: Connection, target: Union[SqlMetric, TableColumn] ) -> None: """ @@ -2130,7 +2132,7 @@ def update_table( # pylint: disable=unused-argument # table is updated. This busts the cache key for all charts that use the table. session.execute(update(SqlaTable).where(SqlaTable.id == target.table.id)) - # if table itself has changed, shadow-writing will happen in `after_udpate` anyway + # if table itself has changed, shadow-writing will happen in `after_update` anyway if target.table not in session.dirty: dataset: NewDataset = ( session.query(NewDataset) @@ -2146,17 +2148,27 @@ def update_table( # pylint: disable=unused-argument # update changed_on timestamp session.execute(update(NewDataset).where(NewDataset.id == dataset.id)) - - # update `Column` model as well - session.add( - target.to_sl_column( - { - target.uuid: session.query(NewColumn) - .filter_by(uuid=target.uuid) - .one_or_none() - } + try: + column = session.query(NewColumn).filter_by(uuid=target.uuid).one() + # update `Column` model as well + session.merge(target.to_sl_column({target.uuid: column})) + except NoResultFound: + logger.warning("No column was found for %s", target) + # see if the column is in cache + column = next( + find_cached_objects_in_session( + session, NewColumn, uuids=[target.uuid] + ), + None, ) - ) + + if not column: + # to be safe, use a different uuid and create a new column + uuid = uuid4() + target.uuid = uuid + column = NewColumn(uuid=uuid) + + session.add(target.to_sl_column({column.uuid: column})) @staticmethod def after_insert( @@ -2441,9 +2453,9 @@ def write_shadow_dataset( sa.event.listen(SqlaTable, "after_insert", SqlaTable.after_insert) sa.event.listen(SqlaTable, "after_delete", SqlaTable.after_delete) sa.event.listen(SqlaTable, "after_update", SqlaTable.after_update) -sa.event.listen(SqlMetric, "after_update", SqlaTable.update_table) +sa.event.listen(SqlMetric, "after_update", SqlaTable.update_column) sa.event.listen(SqlMetric, "after_delete", SqlMetric.after_delete) -sa.event.listen(TableColumn, "after_update", SqlaTable.update_table) +sa.event.listen(TableColumn, "after_update", SqlaTable.update_column) sa.event.listen(TableColumn, "after_delete", TableColumn.after_delete) RLSFilterRoles = Table( diff --git a/superset/connectors/sqla/utils.py b/superset/connectors/sqla/utils.py index 1786c5bf17169..69a983156eafb 100644 --- a/superset/connectors/sqla/utils.py +++ b/superset/connectors/sqla/utils.py @@ -14,6 +14,7 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. +import logging from contextlib import closing from typing import ( Any, @@ -35,6 +36,7 @@ from sqlalchemy.exc import NoSuchTableError from sqlalchemy.ext.declarative import DeclarativeMeta from sqlalchemy.orm import Session +from sqlalchemy.orm.exc import ObjectDeletedError from sqlalchemy.sql.type_api import TypeEngine from superset.errors import ErrorLevel, SupersetError, SupersetErrorType @@ -191,6 +193,7 @@ def get_identifier_quoter(drivername: str) -> Dict[str, Callable[[str], str]]: DeclarativeModel = TypeVar("DeclarativeModel", bound=DeclarativeMeta) +logger = logging.getLogger(__name__) def find_cached_objects_in_session( @@ -209,9 +212,15 @@ def find_cached_objects_in_session( if not ids and not uuids: return iter([]) uuids = uuids or [] + try: + items = set(session) + except ObjectDeletedError: + logger.warning("ObjectDeletedError", exc_info=True) + return iter(()) + return ( item # `session` is an iterator of all known items - for item in set(session) + for item in items if isinstance(item, cls) and (item.id in ids if ids else item.uuid in uuids) ) diff --git a/tests/integration_tests/datasets/api_tests.py b/tests/integration_tests/datasets/api_tests.py index e378811eb97b3..b1767bddad179 100644 --- a/tests/integration_tests/datasets/api_tests.py +++ b/tests/integration_tests/datasets/api_tests.py @@ -35,6 +35,7 @@ DAODeleteFailedError, DAOUpdateFailedError, ) +from superset.datasets.models import Dataset from superset.extensions import db, security_manager from superset.models.core import Database from superset.utils.core import backend, get_example_default_schema @@ -1636,16 +1637,21 @@ def test_import_dataset(self): database = ( db.session.query(Database).filter_by(uuid=database_config["uuid"]).one() ) + shadow_dataset = ( + db.session.query(Dataset).filter_by(uuid=dataset_config["uuid"]).one() + ) assert database.database_name == "imported_database" assert len(database.tables) == 1 dataset = database.tables[0] assert dataset.table_name == "imported_dataset" assert str(dataset.uuid) == dataset_config["uuid"] + assert str(shadow_dataset.uuid) == dataset_config["uuid"] dataset.owners = [] database.owners = [] db.session.delete(dataset) + db.session.delete(shadow_dataset) db.session.delete(database) db.session.commit() diff --git a/tests/integration_tests/datasets/model_tests.py b/tests/integration_tests/datasets/model_tests.py new file mode 100644 index 0000000000000..31abce5494370 --- /dev/null +++ b/tests/integration_tests/datasets/model_tests.py @@ -0,0 +1,69 @@ +# 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. +from unittest import mock + +import pytest +from sqlalchemy.orm.exc import NoResultFound + +from superset.connectors.sqla.models import SqlaTable, TableColumn +from superset.extensions import db +from tests.integration_tests.base_tests import SupersetTestCase +from tests.integration_tests.fixtures.datasource import load_dataset_with_columns + + +class SqlaTableModelTest(SupersetTestCase): + @pytest.mark.usefixtures("load_dataset_with_columns") + def test_dual_update_column(self) -> None: + """ + Test that when updating a sqla ``TableColumn`` + That the shadow ``Column`` is also updated + """ + dataset = db.session.query(SqlaTable).filter_by(table_name="students").first() + column = dataset.columns[0] + column_name = column.column_name + column.column_name = "new_column_name" + SqlaTable.update_column(None, None, target=column) + + # refetch + dataset = db.session.query(SqlaTable).filter_by(id=dataset.id).one() + assert dataset.columns[0].column_name == "new_column_name" + + # reset + column.column_name = column_name + SqlaTable.update_column(None, None, target=column) + + @pytest.mark.usefixtures("load_dataset_with_columns") + @mock.patch("superset.columns.models.Column") + def test_dual_update_column_not_found(self, column_mock) -> None: + """ + Test that when updating a sqla ``TableColumn`` + That the shadow ``Column`` is also updated + """ + dataset = db.session.query(SqlaTable).filter_by(table_name="students").first() + column = dataset.columns[0] + column_uuid = column.uuid + with mock.patch("sqlalchemy.orm.query.Query.one", side_effect=NoResultFound): + SqlaTable.update_column(None, None, target=column) + + # refetch + dataset = db.session.query(SqlaTable).filter_by(id=dataset.id).one() + # it should create a new uuid + assert dataset.columns[0].uuid != column_uuid + + # reset + column.uuid = column_uuid + SqlaTable.update_column(None, None, target=column) diff --git a/tests/integration_tests/fixtures/datasource.py b/tests/integration_tests/fixtures/datasource.py index b6f2476f662c1..574f43d52bbcb 100644 --- a/tests/integration_tests/fixtures/datasource.py +++ b/tests/integration_tests/fixtures/datasource.py @@ -15,10 +15,20 @@ # specific language governing permissions and limitations # under the License. """Fixtures for test_datasource.py""" -from typing import Any, Dict +from typing import Any, Dict, Generator +import pytest +from sqlalchemy import Column, create_engine, Date, Integer, MetaData, String, Table +from sqlalchemy.ext.declarative.api import declarative_base + +from superset.columns.models import Column as Sl_Column +from superset.connectors.sqla.models import SqlaTable, TableColumn +from superset.extensions import db +from superset.models.core import Database +from superset.tables.models import Table as Sl_Table from superset.utils.core import get_example_default_schema from superset.utils.database import get_example_database +from tests.integration_tests.test_app import app def get_datasource_post() -> Dict[str, Any]: @@ -159,3 +169,43 @@ def get_datasource_post() -> Dict[str, Any]: }, ], } + + +@pytest.fixture() +def load_dataset_with_columns() -> Generator[SqlaTable, None, None]: + with app.app_context(): + engine = create_engine(app.config["SQLALCHEMY_DATABASE_URI"], echo=True) + meta = MetaData() + session = db.session + + students = Table( + "students", + meta, + Column("id", Integer, primary_key=True), + Column("name", String(255)), + Column("lastname", String(255)), + Column("ds", Date), + ) + meta.create_all(engine) + + students.insert().values(name="George", ds="2021-01-01") + + dataset = SqlaTable( + database_id=db.session.query(Database).first().id, table_name="students" + ) + column = TableColumn(table_id=dataset.id, column_name="name") + dataset.columns = [column] + session.add(dataset) + session.commit() + yield dataset + + # cleanup + students_table = meta.tables.get("students") + if students_table is not None: + base = declarative_base() + # needed for sqlite + session.commit() + base.metadata.drop_all(engine, [students_table], checkfirst=True) + session.delete(dataset) + session.delete(column) + session.commit() From c3fdd526977318107685e9b9b28540f2eb89227d Mon Sep 17 00:00:00 2001 From: John Bodley <4567245+john-bodley@users.noreply.github.com> Date: Mon, 13 Jun 2022 18:39:15 -0700 Subject: [PATCH 25/71] fix(VERSIONED_EXPORTS): Ensure dashboards and charts adher to the VERSIONED_EXPORTS feature flag (#20368) Co-authored-by: John Bodley --- superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx | 3 ++- superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx index a00ceefbc8761..7dbb30159d91e 100644 --- a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx +++ b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx @@ -153,7 +153,8 @@ function DashboardList(props: DashboardListProps) { const canCreate = hasPerm('can_write'); const canEdit = hasPerm('can_write'); const canDelete = hasPerm('can_write'); - const canExport = hasPerm('can_export'); + const canExport = + hasPerm('can_export') && isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT); const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }]; diff --git a/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx b/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx index 7d6373b935d35..80428a9a13c76 100644 --- a/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx +++ b/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx @@ -174,7 +174,8 @@ const DatasetList: FunctionComponent = ({ const canEdit = hasPerm('can_write'); const canDelete = hasPerm('can_write'); const canCreate = hasPerm('can_write'); - const canExport = hasPerm('can_export'); + const canExport = + hasPerm('can_export') && isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT); const initialSort = SORT_BY; From 86f146e217ddb7c2ebd499acddaa5e8c3b3ab560 Mon Sep 17 00:00:00 2001 From: Kamil Gabryjelski Date: Tue, 14 Jun 2022 15:20:54 +0200 Subject: [PATCH 26/71] feat(explore): Implement viz switcher redesign (#20248) * add icons * Implement fast viz switcher component * Remove unnecessary keys from ControlsPanelContainer * Rename icons * Add unit tests * Add licenses * Fix test * Change BigNumberWithTrendline to BigNumber * fix test * fix test * Add currently rendered viz tile * Move View all charts to the right side * Add license * Fix imports * Fix e2e test --- .../integration/explore/control.test.ts | 10 +- .../assets/images/icons/area-chart-tile.svg | 21 ++ .../assets/images/icons/bar-chart-tile.svg | 21 ++ .../images/icons/big-number-chart-tile.svg | 22 ++ .../images/icons/current-rendered-tile.svg | 21 ++ .../assets/images/icons/line-chart-tile.svg | 21 ++ .../assets/images/icons/pie-chart-tile.svg | 28 ++ .../assets/images/icons/table-chart-tile.svg | 28 ++ .../src/components/Icons/index.tsx | 7 + .../components/ControlPanelsContainer.tsx | 2 - .../VizTypeControl/FastVizSwitcher.tsx | 253 ++++++++++++++++++ .../VizTypeControl/VizTypeControl.test.jsx | 1 + .../VizTypeControl/VizTypeControl.test.tsx | 193 +++++++++++-- .../controls/VizTypeControl/index.tsx | 97 ++++--- .../src/explore/controlPanels/sections.tsx | 2 +- 15 files changed, 644 insertions(+), 83 deletions(-) create mode 100644 superset-frontend/src/assets/images/icons/area-chart-tile.svg create mode 100644 superset-frontend/src/assets/images/icons/bar-chart-tile.svg create mode 100644 superset-frontend/src/assets/images/icons/big-number-chart-tile.svg create mode 100644 superset-frontend/src/assets/images/icons/current-rendered-tile.svg create mode 100644 superset-frontend/src/assets/images/icons/line-chart-tile.svg create mode 100644 superset-frontend/src/assets/images/icons/pie-chart-tile.svg create mode 100644 superset-frontend/src/assets/images/icons/table-chart-tile.svg create mode 100644 superset-frontend/src/explore/components/controls/VizTypeControl/FastVizSwitcher.tsx diff --git a/superset-frontend/cypress-base/cypress/integration/explore/control.test.ts b/superset-frontend/cypress-base/cypress/integration/explore/control.test.ts index e2aec6b7b9873..6bc0840540bed 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/control.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/explore/control.test.ts @@ -99,11 +99,13 @@ describe('VizType control', () => { cy.visitChartByName('Daily Totals'); cy.verifySliceSuccess({ waitAlias: '@tableChartData' }); - cy.get('[data-test="visualization-type"]').contains('Table').click(); + cy.contains('View all charts').click(); - cy.get('button').contains('Evolution').click(); // change categories - cy.get('[role="button"]').contains('Line Chart').click(); - cy.get('button').contains('Select').click(); + cy.get('.ant-modal-content').within(() => { + cy.get('button').contains('Evolution').click(); // change categories + cy.get('[role="button"]').contains('Line Chart').click(); + cy.get('button').contains('Select').click(); + }); cy.get('button[data-test="run-query-button"]').click(); cy.verifySliceSuccess({ diff --git a/superset-frontend/src/assets/images/icons/area-chart-tile.svg b/superset-frontend/src/assets/images/icons/area-chart-tile.svg new file mode 100644 index 0000000000000..dbd747d5e5ad1 --- /dev/null +++ b/superset-frontend/src/assets/images/icons/area-chart-tile.svg @@ -0,0 +1,21 @@ + + + + diff --git a/superset-frontend/src/assets/images/icons/bar-chart-tile.svg b/superset-frontend/src/assets/images/icons/bar-chart-tile.svg new file mode 100644 index 0000000000000..a3aaa6fdd90bb --- /dev/null +++ b/superset-frontend/src/assets/images/icons/bar-chart-tile.svg @@ -0,0 +1,21 @@ + + + + diff --git a/superset-frontend/src/assets/images/icons/big-number-chart-tile.svg b/superset-frontend/src/assets/images/icons/big-number-chart-tile.svg new file mode 100644 index 0000000000000..cd4ac8f2f700e --- /dev/null +++ b/superset-frontend/src/assets/images/icons/big-number-chart-tile.svg @@ -0,0 +1,22 @@ + + + + + diff --git a/superset-frontend/src/assets/images/icons/current-rendered-tile.svg b/superset-frontend/src/assets/images/icons/current-rendered-tile.svg new file mode 100644 index 0000000000000..78f63014e5a1f --- /dev/null +++ b/superset-frontend/src/assets/images/icons/current-rendered-tile.svg @@ -0,0 +1,21 @@ + + + + diff --git a/superset-frontend/src/assets/images/icons/line-chart-tile.svg b/superset-frontend/src/assets/images/icons/line-chart-tile.svg new file mode 100644 index 0000000000000..c6b6b4e403440 --- /dev/null +++ b/superset-frontend/src/assets/images/icons/line-chart-tile.svg @@ -0,0 +1,21 @@ + + + + diff --git a/superset-frontend/src/assets/images/icons/pie-chart-tile.svg b/superset-frontend/src/assets/images/icons/pie-chart-tile.svg new file mode 100644 index 0000000000000..3bd3bf74df4e4 --- /dev/null +++ b/superset-frontend/src/assets/images/icons/pie-chart-tile.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + diff --git a/superset-frontend/src/assets/images/icons/table-chart-tile.svg b/superset-frontend/src/assets/images/icons/table-chart-tile.svg new file mode 100644 index 0000000000000..9a99419d55664 --- /dev/null +++ b/superset-frontend/src/assets/images/icons/table-chart-tile.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + diff --git a/superset-frontend/src/components/Icons/index.tsx b/superset-frontend/src/components/Icons/index.tsx index 27efbe4c2e29f..0761890e4c05b 100644 --- a/superset-frontend/src/components/Icons/index.tsx +++ b/superset-frontend/src/components/Icons/index.tsx @@ -27,6 +27,9 @@ const IconFileNames = [ 'alert', 'alert_solid', 'alert_solid_small', + 'area-chart-tile', + 'bar-chart-tile', + 'big-number-chart-tile', 'binoculars', 'bolt', 'bolt_small', @@ -56,6 +59,7 @@ const IconFileNames = [ 'cog', 'collapse', 'color_palette', + 'current-rendered-tile', 'components', 'copy', 'cursor_target', @@ -101,6 +105,7 @@ const IconFileNames = [ 'keyboard', 'layers', 'lightbulb', + 'line-chart-tile', 'link', 'list', 'list_view', @@ -123,6 +128,7 @@ const IconFileNames = [ 'note', 'offline', 'paperclip', + 'pie-chart-tile', 'placeholder', 'plus', 'plus_large', @@ -141,6 +147,7 @@ const IconFileNames = [ 'sort_desc', 'sort', 'table', + 'table-chart-tile', 'tag', 'trash', 'triangle_change', diff --git a/superset-frontend/src/explore/components/ControlPanelsContainer.tsx b/superset-frontend/src/explore/components/ControlPanelsContainer.tsx index 6ed73f2b3869e..bb2be2b182b61 100644 --- a/superset-frontend/src/explore/components/ControlPanelsContainer.tsx +++ b/superset-frontend/src/explore/components/ControlPanelsContainer.tsx @@ -535,7 +535,6 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => { defaultActiveKey={expandedQuerySections} expandIconPosition="right" ghost - key={`query-sections-${props.form_data.datasource}-${props.form_data.viz_type}`} > {showDatasourceAlert && } {querySections.map(renderControlPanelSection)} @@ -547,7 +546,6 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => { defaultActiveKey={expandedCustomizeSections} expandIconPosition="right" ghost - key={`customize-sections-${props.form_data.datasource}-${props.form_data.viz_type}`} > {customizeSections.map(renderControlPanelSection)} diff --git a/superset-frontend/src/explore/components/controls/VizTypeControl/FastVizSwitcher.tsx b/superset-frontend/src/explore/components/controls/VizTypeControl/FastVizSwitcher.tsx new file mode 100644 index 0000000000000..5b9df8021fa16 --- /dev/null +++ b/superset-frontend/src/explore/components/controls/VizTypeControl/FastVizSwitcher.tsx @@ -0,0 +1,253 @@ +/** + * 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, { + ReactElement, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { useSelector } from 'react-redux'; +import { css, SupersetTheme, t, useTheme } from '@superset-ui/core'; +import { usePluginContext } from 'src/components/DynamicPlugins'; +import { Tooltip } from 'src/components/Tooltip'; +import Icons from 'src/components/Icons'; +import { ExplorePageState } from 'src/explore/reducers/getInitialState'; + +export interface VizMeta { + icon: ReactElement; + name: string; +} + +export interface FastVizSwitcherProps { + onChange: (vizName: string) => void; + currentSelection: string | null; +} +interface VizTileProps { + vizMeta: VizMeta; + isActive: boolean; + isRendered: boolean; + onTileClick: (vizType: string) => void; +} + +const FEATURED_CHARTS: VizMeta[] = [ + { + name: 'echarts_timeseries_line', + icon: , + }, + { name: 'table', icon: }, + { + name: 'big_number_total', + icon: , + }, + { name: 'pie', icon: }, + { + name: 'echarts_timeseries_bar', + icon: , + }, + { name: 'echarts_area', icon: }, +]; + +const VizTile = ({ + isActive, + isRendered, + vizMeta, + onTileClick, +}: VizTileProps) => { + const { mountedPluginMetadata } = usePluginContext(); + const chartNameRef = useRef(null); + const theme = useTheme(); + const TILE_TRANSITION_TIME = theme.transitionTiming * 2; + const [tooltipVisible, setTooltipVisible] = useState(false); + const [isTransitioning, setIsTransitioning] = useState(false); + const [showTooltip, setShowTooltip] = useState(false); + const chartName = vizMeta.name + ? mountedPluginMetadata[vizMeta.name]?.name || `${vizMeta.name}` + : t('Select Viz Type'); + + const handleTileClick = useCallback(() => { + onTileClick(vizMeta.name); + setIsTransitioning(true); + setTooltipVisible(false); + setTimeout(() => { + setIsTransitioning(false); + }, TILE_TRANSITION_TIME * 1000); + }, [onTileClick, TILE_TRANSITION_TIME, vizMeta.name]); + + // Antd tooltip seems to be bugged - when elements move, the tooltip sometimes + // stays visible even when user doesn't hover over the element. + // Here we manually prevent it from displaying after user triggers transition + useEffect(() => { + setShowTooltip( + Boolean( + !isTransitioning && + (!isActive || + (chartNameRef.current && + chartNameRef.current.scrollWidth > + chartNameRef.current.clientWidth)), + ), + ); + }, [isActive, isTransitioning]); + + const containerProps = useMemo( + () => + !isActive + ? { role: 'button', tabIndex: 0, onClick: handleTileClick } + : {}, + [handleTileClick, isActive], + ); + + let tooltipTitle: string | null = null; + if (showTooltip) { + tooltipTitle = isRendered + ? t('Currently rendered: %s', chartName) + : chartName; + } + return ( + setTooltipVisible(visible)} + visible={tooltipVisible && !isTransitioning} + placement="top" + mouseEnterDelay={0.4} + > +
+ {vizMeta.icon}{' '} + + {chartName} + +
+
+ ); +}; + +export const FastVizSwitcher = React.memo( + ({ currentSelection, onChange }: FastVizSwitcherProps) => { + const currentViz = useSelector( + state => + state.charts && + Object.values(state.charts)[0]?.latestQueryFormData?.viz_type, + ); + const vizTiles = useMemo(() => { + const vizTiles = [...FEATURED_CHARTS]; + if ( + currentSelection && + FEATURED_CHARTS.every( + featuredVizMeta => featuredVizMeta.name !== currentSelection, + ) && + currentSelection !== currentViz + ) { + vizTiles.unshift({ + name: currentSelection, + icon: ( + css` + padding: ${theme.gridUnit}px; + & > * { + line-height: 0; + } + `} + /> + ), + }); + } + if ( + currentViz && + FEATURED_CHARTS.every( + featuredVizMeta => featuredVizMeta.name !== currentViz, + ) + ) { + vizTiles.unshift({ + name: currentViz, + icon: , + }); + } + return vizTiles; + }, [currentSelection, currentViz]); + + return ( +
css` + display: flex; + justify-content: space-between; + column-gap: ${theme.gridUnit}px; + `} + data-test="fast-viz-switcher" + > + {vizTiles.map(vizMeta => ( + + ))} +
+ ); + }, +); diff --git a/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeControl.test.jsx b/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeControl.test.jsx index c027fd0c3dff1..67a6a19333942 100644 --- a/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeControl.test.jsx +++ b/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeControl.test.jsx @@ -67,6 +67,7 @@ describe('VizTypeControl', () => { , + { useRedux: true }, ); await waitForEffects(); }); diff --git a/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeControl.test.tsx b/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeControl.test.tsx index a8755916208cb..98a7b9099e728 100644 --- a/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeControl.test.tsx +++ b/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeControl.test.tsx @@ -17,21 +17,22 @@ * under the License. */ import { Preset } from '@superset-ui/core'; -import { render, cleanup, screen, waitFor } from 'spec/helpers/testing-library'; -import { Provider } from 'react-redux'; -import { - getMockStore, - mockStore, - stateWithoutNativeFilters, -} from 'spec/fixtures/mockStore'; +import { render, cleanup, screen, within } from 'spec/helpers/testing-library'; +import { stateWithoutNativeFilters } from 'spec/fixtures/mockStore'; import React from 'react'; import userEvent from '@testing-library/user-event'; import { DynamicPluginProvider } from 'src/components/DynamicPlugins'; import { testWithId } from 'src/utils/testUtils'; import { + BigNumberTotalChartPlugin, + EchartsAreaChartPlugin, EchartsMixedTimeseriesChartPlugin, + EchartsPieChartPlugin, + EchartsTimeseriesBarChartPlugin, EchartsTimeseriesChartPlugin, + EchartsTimeseriesLineChartPlugin, } from '@superset-ui/plugin-chart-echarts'; +import TableChartPlugin from '@superset-ui/plugin-chart-table'; import { LineChartPlugin } from '@superset-ui/preset-chart-xy'; import TimeTableChartPlugin from '../../../../visualizations/TimeTable'; import VizTypeControl, { VIZ_TYPE_CONTROL_TEST_ID } from './index'; @@ -44,6 +45,18 @@ class MainPreset extends Preset { name: 'Legacy charts', plugins: [ new LineChartPlugin().configure({ key: 'line' }), + new TableChartPlugin().configure({ key: 'table' }), + new BigNumberTotalChartPlugin().configure({ key: 'big_number_total' }), + new EchartsTimeseriesLineChartPlugin().configure({ + key: 'echarts_timeseries_line', + }), + new EchartsAreaChartPlugin().configure({ + key: 'echarts_area', + }), + new EchartsTimeseriesBarChartPlugin().configure({ + key: 'echarts_timeseries_bar', + }), + new EchartsPieChartPlugin().configure({ key: 'pie' }), new EchartsTimeseriesChartPlugin().configure({ key: 'echarts_timeseries', }), @@ -67,7 +80,7 @@ const getTestId = testWithId(VIZ_TYPE_CONTROL_TEST_ID, true); describe('VizTypeControl', () => { new MainPreset().register(); - const newVizTypeControlProps = { + const defaultProps = { description: '', label: '', name: '', @@ -75,20 +88,17 @@ describe('VizTypeControl', () => { labelType: 'primary', onChange: jest.fn(), isModalOpenInit: true, - } as const; + }; const renderWrapper = ( - props = newVizTypeControlProps, + props: typeof defaultProps = defaultProps, state: object = stateWithoutNativeFilters, ) => { render( - - - - - , + + + , + { useRedux: true, initialState: state }, ); }; @@ -97,6 +107,119 @@ describe('VizTypeControl', () => { jest.clearAllMocks(); }); + it('Fast viz switcher tiles render', () => { + const props = { + ...defaultProps, + value: 'echarts_timeseries_line', + isModalOpenInit: false, + }; + renderWrapper(props); + expect(screen.getByLabelText('line-chart-tile')).toBeVisible(); + expect(screen.getByLabelText('table-chart-tile')).toBeVisible(); + expect(screen.getByLabelText('big-number-chart-tile')).toBeVisible(); + expect(screen.getByLabelText('pie-chart-tile')).toBeVisible(); + expect(screen.getByLabelText('bar-chart-tile')).toBeVisible(); + expect(screen.getByLabelText('area-chart-tile')).toBeVisible(); + expect(screen.queryByLabelText('monitor')).not.toBeInTheDocument(); + expect( + screen.queryByLabelText('current-rendered-tile'), + ).not.toBeInTheDocument(); + + expect( + within(screen.getByTestId('fast-viz-switcher')).getByText( + 'Time-series Line Chart', + ), + ).toBeInTheDocument(); + expect( + within(screen.getByTestId('fast-viz-switcher')).getByText('Table'), + ).toBeInTheDocument(); + expect( + within(screen.getByTestId('fast-viz-switcher')).getByText('Big Number'), + ).toBeInTheDocument(); + expect( + within(screen.getByTestId('fast-viz-switcher')).getByText('Pie Chart'), + ).toBeInTheDocument(); + expect( + within(screen.getByTestId('fast-viz-switcher')).getByText( + 'Time-series Bar Chart v2', + ), + ).toBeInTheDocument(); + expect( + within(screen.getByTestId('fast-viz-switcher')).getByText( + 'Time-series Area Chart', + ), + ).toBeInTheDocument(); + expect( + within(screen.getByTestId('fast-viz-switcher')).queryByText('Line Chart'), + ).not.toBeInTheDocument(); + }); + + it('Render viz tiles when non-featured chart is selected', () => { + const props = { + ...defaultProps, + value: 'line', + isModalOpenInit: false, + }; + renderWrapper(props); + + expect(screen.getByLabelText('monitor')).toBeVisible(); + expect( + within(screen.getByTestId('fast-viz-switcher')).getByText('Line Chart'), + ).toBeVisible(); + }); + + it('Render viz tiles when non-featured is rendered', () => { + const props = { + ...defaultProps, + value: 'line', + isModalOpenInit: false, + }; + const state = { + charts: { + 1: { + latestQueryFormData: { + viz_type: 'line', + }, + }, + }, + }; + renderWrapper(props, state); + expect(screen.getByLabelText('current-rendered-tile')).toBeVisible(); + expect( + within(screen.getByTestId('fast-viz-switcher')).getByText('Line Chart'), + ).toBeVisible(); + }); + + it('Change viz type on click', () => { + const props = { + ...defaultProps, + value: 'echarts_timeseries_line', + isModalOpenInit: false, + }; + renderWrapper(props); + userEvent.click( + within(screen.getByTestId('fast-viz-switcher')).getByText( + 'Time-series Line Chart', + ), + ); + expect(props.onChange).not.toHaveBeenCalled(); + userEvent.click( + within(screen.getByTestId('fast-viz-switcher')).getByText('Table'), + ); + expect(props.onChange).toHaveBeenCalledWith('table'); + }); + + it('Open viz gallery modal on "View all charts" click', async () => { + renderWrapper({ ...defaultProps, isModalOpenInit: false }); + expect( + screen.queryByText('Select a visualization type'), + ).not.toBeInTheDocument(); + userEvent.click(screen.getByText('View all charts')); + expect( + await screen.findByText('Select a visualization type'), + ).toBeVisible(); + }); + it('Search visualization type', async () => { renderWrapper(); @@ -104,20 +227,38 @@ describe('VizTypeControl', () => { userEvent.click(screen.getByRole('button', { name: 'ballot All charts' })); - await waitFor(() => { - expect(visualizations).toHaveTextContent(/Time-series Table/); - }); + expect( + await within(visualizations).findByText('Time-series Line Chart'), + ).toBeVisible(); // search userEvent.type( screen.getByTestId(getTestId('search-input')), 'time series', ); - await waitFor(() => { - expect(visualizations).toHaveTextContent(/Time-series Table/); - expect(visualizations).toHaveTextContent(/Time-series Chart/); - expect(visualizations).toHaveTextContent(/Mixed Time-Series/); - expect(visualizations).not.toHaveTextContent(/Line Chart/); - }); + expect( + await within(visualizations).findByText('Time-series Table'), + ).toBeVisible(); + expect(within(visualizations).getByText('Time-series Chart')).toBeVisible(); + expect(within(visualizations).getByText('Mixed Time-Series')).toBeVisible(); + expect( + within(visualizations).getByText('Time-series Area Chart'), + ).toBeVisible(); + expect( + within(visualizations).getByText('Time-series Line Chart'), + ).toBeVisible(); + expect( + within(visualizations).getByText('Time-series Bar Chart v2'), + ).toBeVisible(); + expect( + within(visualizations).queryByText('Line Chart'), + ).not.toBeInTheDocument(); + expect(within(visualizations).queryByText('Table')).not.toBeInTheDocument(); + expect( + within(visualizations).queryByText('Big Number'), + ).not.toBeInTheDocument(); + expect( + within(visualizations).queryByText('Pie Chart'), + ).not.toBeInTheDocument(); }); }); diff --git a/superset-frontend/src/explore/components/controls/VizTypeControl/index.tsx b/superset-frontend/src/explore/components/controls/VizTypeControl/index.tsx index 7837eb04ae9f0..0f8de76926c0a 100644 --- a/superset-frontend/src/explore/components/controls/VizTypeControl/index.tsx +++ b/superset-frontend/src/explore/components/controls/VizTypeControl/index.tsx @@ -17,25 +17,20 @@ * under the License. */ import React, { useCallback, useState } from 'react'; -import PropTypes from 'prop-types'; -import { t, getChartMetadataRegistry, styled } from '@superset-ui/core'; +import { + css, + t, + getChartMetadataRegistry, + styled, + SupersetTheme, +} from '@superset-ui/core'; import { usePluginContext } from 'src/components/DynamicPlugins'; import Modal from 'src/components/Modal'; -import { Tooltip } from 'src/components/Tooltip'; -import Label, { Type } from 'src/components/Label'; -import ControlHeader from 'src/explore/components/ControlHeader'; +import { noOp } from 'src/utils/common'; import VizTypeGallery, { MAX_ADVISABLE_VIZ_GALLERY_WIDTH, } from './VizTypeGallery'; - -const propTypes = { - description: PropTypes.string, - label: PropTypes.string, - name: PropTypes.string.isRequired, - onChange: PropTypes.func, - value: PropTypes.string.isRequired, - labelType: PropTypes.string, -}; +import { FastVizSwitcher } from './FastVizSwitcher'; interface VizTypeControlProps { description?: string; @@ -43,15 +38,9 @@ interface VizTypeControlProps { name: string; onChange: (vizType: string | null) => void; value: string | null; - labelType?: Type; isModalOpenInit?: boolean; } -const defaultProps = { - onChange: () => {}, - labelType: 'default', -}; - const metadataRegistry = getChartMetadataRegistry(); export const VIZ_TYPE_CONTROL_TEST_ID = 'viz-type-control'; @@ -62,7 +51,14 @@ function VizSupportValidation({ vizType }: { vizType: string }) { return null; } return ( -
+
+ css` + margin-top: ${theme.gridUnit}px; + ` + } + > {' '} {t('This visualization type is not supported.')}
@@ -76,9 +72,11 @@ const UnpaddedModal = styled(Modal)` `; /** Manages the viz type and the viz picker modal */ -const VizTypeControl = (props: VizTypeControlProps) => { - const { value: initialValue, onChange, isModalOpenInit, labelType } = props; - const { mountedPluginMetadata } = usePluginContext(); +const VizTypeControl = ({ + value: initialValue, + onChange = noOp, + isModalOpenInit, +}: VizTypeControlProps) => { const [showModal, setShowModal] = useState(!!isModalOpenInit); // a trick to force re-initialization of the gallery each time the modal opens, // ensuring that the modal always opens to the correct category. @@ -101,30 +99,32 @@ const VizTypeControl = (props: VizTypeControlProps) => { setSelectedViz(initialValue); }, [initialValue]); - const labelContent = initialValue - ? mountedPluginMetadata[initialValue]?.name || `${initialValue}` - : t('Select Viz Type'); - return ( -
- - +
css` + min-width: ${theme.gridUnit * 72}px; + max-width: fit-content; + `} > - <> - - {initialValue && } - - - + + {initialValue && } +
+
+ css` + display: flex; + justify-content: flex-end; + margin-top: ${theme.gridUnit * 3}px; + color: ${theme.colors.grayscale.base}; + text-decoration: underline; + ` + } + > + + {t('View all charts')} + +
{ onChange={setSelectedViz} /> -
+ ); }; -VizTypeControl.propTypes = propTypes; -VizTypeControl.defaultProps = defaultProps; - export default VizTypeControl; diff --git a/superset-frontend/src/explore/controlPanels/sections.tsx b/superset-frontend/src/explore/controlPanels/sections.tsx index be21747ed63e6..78815215df228 100644 --- a/superset-frontend/src/explore/controlPanels/sections.tsx +++ b/superset-frontend/src/explore/controlPanels/sections.tsx @@ -29,7 +29,7 @@ export const druidTimeSeries: ControlPanelSectionConfig = { }; export const datasourceAndVizType: ControlPanelSectionConfig = { - label: t('Chart type'), + label: t('Visualization type'), expanded: true, controlSetRows: [ ['datasource'], From 160e674b9049c006d3fada3e99a89a2c9dbe80ac Mon Sep 17 00:00:00 2001 From: Phillip Kelley-Dotson Date: Tue, 14 Jun 2022 12:10:01 -0700 Subject: [PATCH 27/71] fix: update connection modal to use existing catalog (#20372) * fix: update connection modal to use existing catalog * remove console --- .../src/views/CRUD/data/database/DatabaseModal/index.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.tsx b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.tsx index 2b97ed791b792..ca387a28a069f 100644 --- a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.tsx +++ b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.tsx @@ -366,18 +366,17 @@ function dbReducer( configuration_method: action.payload.configuration_method, extra_json: deserializeExtraJSON, catalog: engineParamsCatalog, - parameters: action.payload.parameters, + parameters: action.payload.parameters || trimmedState.parameters, query_input, }; } - return { ...action.payload, encrypted_extra: action.payload.encrypted_extra || '', engine: action.payload.backend || trimmedState.engine, configuration_method: action.payload.configuration_method, extra_json: deserializeExtraJSON, - parameters: action.payload.parameters, + parameters: action.payload.parameters || trimmedState.parameters, query_input, }; From ead10401e7f5344d821ee3086c191fedb5d6ee4b Mon Sep 17 00:00:00 2001 From: Diego Medina Date: Tue, 14 Jun 2022 20:47:37 -0300 Subject: [PATCH 28/71] fix: A newly connected database doesn't appear in the databases list if user connected database using the 'plus' button (#20363) * fix: A newly connected database doesn't appear in the databases list if user connected database using the 'plus' button * PR comments --- .../src/views/components/Menu.test.tsx | 6 +++ .../src/views/components/MenuRight.tsx | 50 ++++++++++++++++--- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/superset-frontend/src/views/components/Menu.test.tsx b/superset-frontend/src/views/components/Menu.test.tsx index 0d84d2c663ccc..31aad0be8a1e3 100644 --- a/superset-frontend/src/views/components/Menu.test.tsx +++ b/superset-frontend/src/views/components/Menu.test.tsx @@ -476,3 +476,9 @@ test('should hide create button without proper roles', () => { render(
, { useRedux: true, useQueryParams: true }); expect(screen.queryByTestId('new-dropdown')).not.toBeInTheDocument(); }); + +test('should render without QueryParamProvider', () => { + useSelectorMock.mockReturnValue({ roles: [] }); + render(, { useRedux: true }); + expect(screen.queryByTestId('new-dropdown')).not.toBeInTheDocument(); +}); diff --git a/superset-frontend/src/views/components/MenuRight.tsx b/superset-frontend/src/views/components/MenuRight.tsx index 61bc6de0d6926..36c5dd37fda2f 100644 --- a/superset-frontend/src/views/components/MenuRight.tsx +++ b/superset-frontend/src/views/components/MenuRight.tsx @@ -88,7 +88,10 @@ const RightMenu = ({ settings, navbarRight, isFrontendRoute, -}: RightMenuProps) => { + setQuery, +}: RightMenuProps & { + setQuery: ({ databaseAdded }: { databaseAdded: boolean }) => void; +}) => { const user = useSelector( state => state.user, ); @@ -96,10 +99,6 @@ const RightMenu = ({ state => state.dashboardInfo?.id, ); - const [, setQuery] = useQueryParams({ - databaseAdded: BooleanParam, - }); - const { roles } = user; const { CSV_EXTENSIONS, @@ -439,4 +438,43 @@ const RightMenu = ({ ); }; -export default RightMenu; +const RightMenuWithQueryWrapper: React.FC = props => { + const [, setQuery] = useQueryParams({ + databaseAdded: BooleanParam, + }); + + return ; +}; + +// Query param manipulation requires that, during the setup, the +// QueryParamProvider is present and configured. +// Superset still has multiple entry points, and not all of them have +// the same setup, and critically, not all of them have the QueryParamProvider. +// This wrapper ensures the RightMenu renders regardless of the provider being present. +class RightMenuErrorWrapper extends React.PureComponent { + state = { + hasError: false, + }; + + static getDerivedStateFromError() { + return { hasError: true }; + } + + noop = () => {}; + + render() { + if (this.state.hasError) { + return ; + } + + return this.props.children; + } +} + +const RightMenuWrapper: React.FC = props => ( + + + +); + +export default RightMenuWrapper; From 3fe53f735e645a96ca5158054b5c9cb26e3e83e7 Mon Sep 17 00:00:00 2001 From: Diego Medina Date: Wed, 15 Jun 2022 03:27:24 -0300 Subject: [PATCH 29/71] fix: Unable to export multiple Dashboards with the same name (#20383) --- superset/dashboards/commands/export.py | 2 +- .../integration_tests/dashboards/commands_tests.py | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/superset/dashboards/commands/export.py b/superset/dashboards/commands/export.py index c7aa8b6e5c65b..2d131d8f84e1c 100644 --- a/superset/dashboards/commands/export.py +++ b/superset/dashboards/commands/export.py @@ -112,7 +112,7 @@ def _export( model: Dashboard, export_related: bool = True ) -> Iterator[Tuple[str, str]]: dashboard_slug = secure_filename(model.dashboard_title) - file_name = f"dashboards/{dashboard_slug}.yaml" + file_name = f"dashboards/{dashboard_slug}_{model.id}.yaml" payload = model.export_to_dict( recursive=False, diff --git a/tests/integration_tests/dashboards/commands_tests.py b/tests/integration_tests/dashboards/commands_tests.py index e3e6971155b9f..d382a5f50d1b2 100644 --- a/tests/integration_tests/dashboards/commands_tests.py +++ b/tests/integration_tests/dashboards/commands_tests.py @@ -68,7 +68,7 @@ def test_export_dashboard_command(self, mock_g1, mock_g2): expected_paths = { "metadata.yaml", - "dashboards/World_Banks_Data.yaml", + f"dashboards/World_Banks_Data_{example_dashboard.id}.yaml", "datasets/examples/wb_health_population.yaml", "databases/examples.yaml", } @@ -77,7 +77,9 @@ def test_export_dashboard_command(self, mock_g1, mock_g2): expected_paths.add(f"charts/{chart_slug}_{chart.id}.yaml") assert expected_paths == set(contents.keys()) - metadata = yaml.safe_load(contents["dashboards/World_Banks_Data.yaml"]) + metadata = yaml.safe_load( + contents[f"dashboards/World_Banks_Data_{example_dashboard.id}.yaml"] + ) # remove chart UUIDs from metadata so we can compare for chart_info in metadata["position"].values(): @@ -269,7 +271,9 @@ def test_export_dashboard_command_key_order(self, mock_g1, mock_g2): command = ExportDashboardsCommand([example_dashboard.id]) contents = dict(command.run()) - metadata = yaml.safe_load(contents["dashboards/World_Banks_Data.yaml"]) + metadata = yaml.safe_load( + contents[f"dashboards/World_Banks_Data_{example_dashboard.id}.yaml"] + ) assert list(metadata.keys()) == [ "dashboard_title", "description", @@ -284,7 +288,7 @@ def test_export_dashboard_command_key_order(self, mock_g1, mock_g2): @pytest.mark.usefixtures("load_world_bank_dashboard_with_slices") @patch("superset.dashboards.commands.export.suffix") def test_append_charts(self, mock_suffix): - """Test that oprhaned charts are added to the dashbaord position""" + """Test that orphaned charts are added to the dashboard position""" # return deterministic IDs mock_suffix.side_effect = (str(i) for i in itertools.count(1)) @@ -435,7 +439,7 @@ def test_export_dashboard_command_no_related(self, mock_g1, mock_g2): expected_paths = { "metadata.yaml", - "dashboards/World_Banks_Data.yaml", + f"dashboards/World_Banks_Data_{example_dashboard.id}.yaml", } assert expected_paths == set(contents.keys()) From df8bb46ee26807a06e168b3a234e43b02bf658e1 Mon Sep 17 00:00:00 2001 From: smileydev <47900232+prosdev0107@users.noreply.github.com> Date: Wed, 15 Jun 2022 02:28:54 -0400 Subject: [PATCH 30/71] fix(fonts): Show the all the A's in our workspace correctly, not funky (#20361) * fix(fonts): make to fix the issue of broking the Inter font char A * fix(css & fonts): make to install from npmjs.org --- superset-frontend/package-lock.json | 14 +++++++------- superset-frontend/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 31416e0842d82..ce70b2368a3ab 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -20,7 +20,7 @@ "@emotion/cache": "^11.4.0", "@emotion/react": "^11.4.1", "@emotion/styled": "^11.3.0", - "@fontsource/inter": "^4.5.7", + "@fontsource/inter": "^4.0.0", "@superset-ui/chart-controls": "file:./packages/superset-ui-chart-controls", "@superset-ui/core": "file:./packages/superset-ui-core", "@superset-ui/legacy-plugin-chart-calendar": "file:./plugins/legacy-plugin-chart-calendar", @@ -5652,9 +5652,9 @@ } }, "node_modules/@fontsource/inter": { - "version": "4.5.7", - "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-4.5.7.tgz", - "integrity": "sha512-25k3thupaOEBexuU+jAkGqieKPbuhSuA+sinDwp1iBNhqQPiJ9QHDvsXgoCgCbZ4sGlE8aCwZmSlDJrPdJHNkw==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-4.0.0.tgz", + "integrity": "sha512-zc9DDGEz0cgftT6VbHPrdBBVaBQrK4P6UDuuNrib1KNnbDCY1zHTMwYiN2XH6SFDufRKnsjUR5cEeWDANDDaYw==" }, "node_modules/@gar/promisify": { "version": "1.1.2", @@ -59687,9 +59687,9 @@ } }, "@fontsource/inter": { - "version": "4.5.7", - "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-4.5.7.tgz", - "integrity": "sha512-25k3thupaOEBexuU+jAkGqieKPbuhSuA+sinDwp1iBNhqQPiJ9QHDvsXgoCgCbZ4sGlE8aCwZmSlDJrPdJHNkw==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-4.0.0.tgz", + "integrity": "sha512-zc9DDGEz0cgftT6VbHPrdBBVaBQrK4P6UDuuNrib1KNnbDCY1zHTMwYiN2XH6SFDufRKnsjUR5cEeWDANDDaYw==" }, "@gar/promisify": { "version": "1.1.2", diff --git a/superset-frontend/package.json b/superset-frontend/package.json index acdf9952d0998..7cccb005bec47 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -80,7 +80,7 @@ "@emotion/cache": "^11.4.0", "@emotion/react": "^11.4.1", "@emotion/styled": "^11.3.0", - "@fontsource/inter": "^4.5.7", + "@fontsource/inter": "^4.0.0", "@superset-ui/chart-controls": "file:./packages/superset-ui-chart-controls", "@superset-ui/core": "file:./packages/superset-ui-core", "@superset-ui/legacy-plugin-chart-calendar": "file:./plugins/legacy-plugin-chart-calendar", From 7c252d75240559d0bba9be3be8419b65b86967df Mon Sep 17 00:00:00 2001 From: Yongjie Zhao Date: Wed, 15 Jun 2022 20:55:10 +0800 Subject: [PATCH 31/71] feat: adding truncate metric control on timeseries charts (#20373) --- .../src/operators/renameOperator.ts | 7 ++++-- .../src/shared-controls/index.tsx | 8 +++++++ .../test/operators/renameOperator.test.ts | 22 +++++++++++++++++++ .../src/MixedTimeseries/controlPanel.tsx | 9 ++++++++ .../src/Timeseries/Area/controlPanel.tsx | 1 + .../Timeseries/Regular/Bar/controlPanel.tsx | 1 + .../Regular/Scatter/controlPanel.tsx | 1 + .../src/Timeseries/Regular/controlPanel.tsx | 1 + .../src/Timeseries/Step/controlPanel.tsx | 1 + .../src/Timeseries/controlPanel.tsx | 1 + .../test/MixedTimeseries/buildQuery.test.ts | 2 ++ 11 files changed, 52 insertions(+), 2 deletions(-) diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/operators/renameOperator.ts b/superset-frontend/packages/superset-ui-chart-controls/src/operators/renameOperator.ts index 94dfa70bbc8f2..84cbbce8c5fdb 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/operators/renameOperator.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/operators/renameOperator.ts @@ -32,12 +32,13 @@ export const renameOperator: PostProcessingFactory = ( ) => { const metrics = ensureIsArray(queryObject.metrics); const columns = ensureIsArray(queryObject.columns); - const { x_axis: xAxis } = formData; + const { x_axis: xAxis, truncate_metric } = formData; // remove or rename top level of column name(metric name) in the MultiIndex when // 1) only 1 metric // 2) exist dimentsion // 3) exist xAxis // 4) exist time comparison, and comparison type is "actual values" + // 5) truncate_metric in form_data and truncate_metric is true if ( metrics.length === 1 && columns.length > 0 && @@ -52,7 +53,9 @@ export const renameOperator: PostProcessingFactory = ( ComparisionType.Percentage, ].includes(formData.comparison_type) ) - ) + ) && + truncate_metric !== undefined && + !!truncate_metric ) { const renamePairs: [string, string | null][] = []; diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx index 5ff32d50b0785..c5bc9d56d4805 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx @@ -533,6 +533,13 @@ const color_scheme: SharedControlConfig<'ColorSchemeControl'> = { }), }; +const truncate_metric: SharedControlConfig<'CheckboxControl'> = { + type: 'CheckboxControl', + label: t('Truncate Metric'), + default: true, + description: t('Whether to truncate metrics'), +}; + const enableExploreDnd = isFeatureEnabled( FeatureFlag.ENABLE_EXPLORE_DRAG_AND_DROP, ); @@ -571,6 +578,7 @@ const sharedControls = { series_limit, series_limit_metric: enableExploreDnd ? dnd_sort_by : sort_by, legacy_order_by: enableExploreDnd ? dnd_sort_by : sort_by, + truncate_metric, }; export { sharedControls, dndEntity, dndColumnsControl }; diff --git a/superset-frontend/packages/superset-ui-chart-controls/test/operators/renameOperator.test.ts b/superset-frontend/packages/superset-ui-chart-controls/test/operators/renameOperator.test.ts index 2c32e0791ba17..26bbe9e3695cc 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/test/operators/renameOperator.test.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/test/operators/renameOperator.test.ts @@ -27,6 +27,7 @@ const formData: SqlaFormData = { granularity: 'month', datasource: 'foo', viz_type: 'table', + truncate_metric: true, }; const queryObject: QueryObject = { is_timeseries: true, @@ -144,3 +145,24 @@ test('should add renameOperator if exist "actual value" time comparison', () => }, }); }); + +test('should remove renameOperator', () => { + expect( + renameOperator( + { + ...formData, + truncate_metric: false, + }, + queryObject, + ), + ).toEqual(undefined); + expect( + renameOperator( + { + ...formData, + truncate_metric: undefined, + }, + queryObject, + ), + ).toEqual(undefined); +}); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx index 74c0ac8890261..fb164f1a26e81 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx @@ -123,6 +123,15 @@ function createQuerySection( }, }, ], + [ + { + name: `truncate_metric${controlSuffix}`, + config: { + ...sharedControls.truncate_metric, + default: sharedControls.truncate_metric.default, + }, + }, + ], ], }; } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx index c2aeb2916b47e..c8922dd11e58b 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx @@ -87,6 +87,7 @@ const config: ControlPanelConfig = { ['timeseries_limit_metric'], ['order_desc'], ['row_limit'], + ['truncate_metric'], ], }, sections.advancedAnalyticsControls, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx index f0c8aa52ac0ea..2080fceef65df 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx @@ -296,6 +296,7 @@ const config: ControlPanelConfig = { ['timeseries_limit_metric'], ['order_desc'], ['row_limit'], + ['truncate_metric'], ], }, sections.advancedAnalyticsControls, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx index 52e799309890d..fd2fa79651305 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx @@ -62,6 +62,7 @@ const config: ControlPanelConfig = { ['timeseries_limit_metric'], ['order_desc'], ['row_limit'], + ['truncate_metric'], ], }, sections.advancedAnalyticsControls, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/controlPanel.tsx index c56c4a2ab20ab..8dc34861b1335 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/controlPanel.tsx @@ -79,6 +79,7 @@ const config: ControlPanelConfig = { ['timeseries_limit_metric'], ['order_desc'], ['row_limit'], + ['truncate_metric'], ], }, sections.advancedAnalyticsControls, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx index 94d179c5a4ab7..2902937333860 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx @@ -85,6 +85,7 @@ const config: ControlPanelConfig = { ['timeseries_limit_metric'], ['order_desc'], ['row_limit'], + ['truncate_metric'], ], }, sections.advancedAnalyticsControls, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/controlPanel.tsx index 843affc3d07e4..d039a059c590a 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/controlPanel.tsx @@ -86,6 +86,7 @@ const config: ControlPanelConfig = { ['timeseries_limit_metric'], ['order_desc'], ['row_limit'], + ['truncate_metric'], ], }, sections.advancedAnalyticsControls, diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/MixedTimeseries/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/MixedTimeseries/buildQuery.test.ts index eb95a4f71df1e..0b766c2dc4e43 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/MixedTimeseries/buildQuery.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/MixedTimeseries/buildQuery.test.ts @@ -47,6 +47,7 @@ const formDataMixedChart = { timeseries_limit_metric: 'count', order_desc: true, emit_filter: true, + truncate_metric: true, // -- query b groupby_b: [], metrics_b: ['count'], @@ -62,6 +63,7 @@ const formDataMixedChart = { timeseries_limit_metric_b: undefined, order_desc_b: false, emit_filter_b: undefined, + truncate_metric_b: true, // chart configs show_value: false, show_valueB: undefined, From 0a50a9b3804837ea7130f91bfcfcca57ab50129f Mon Sep 17 00:00:00 2001 From: Yongjie Zhao Date: Wed, 15 Jun 2022 21:49:00 +0800 Subject: [PATCH 32/71] feat: setting limit value when Pie chart switches (#20392) --- .../plugins/plugin-chart-echarts/src/Pie/controlPanel.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/controlPanel.tsx index 6e3a1ba593a2a..43e1ab6cba4d3 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/controlPanel.tsx @@ -17,7 +17,7 @@ * under the License. */ import React from 'react'; -import { t, validateNonEmpty } from '@superset-ui/core'; +import { ensureIsInt, t, validateNonEmpty } from '@superset-ui/core'; import { ControlPanelConfig, ControlPanelsContainerProps, @@ -257,6 +257,8 @@ const config: ControlPanelConfig = { ...formData, metric: formData.standardizedFormData.standardizedState.metrics[0], groupby: formData.standardizedFormData.standardizedState.columns, + row_limit: + ensureIsInt(formData.row_limit, 100) >= 100 ? 100 : formData.row_limit, }), }; From d6f9fb5af113dce54e73432e4c13eca0bafbee6f Mon Sep 17 00:00:00 2001 From: "Michael S. Molina" <70410625+michael-s-molina@users.noreply.github.com> Date: Wed, 15 Jun 2022 11:01:45 -0300 Subject: [PATCH 33/71] chore: Removes unused vars (#20194) * chore: Removes unused vars * Fixes lint * Disables no-unused-vars for the operators --- .../dashboard/nativeFilters.test.ts | 7 ---- .../integration/dashboard/save.test.js | 4 +-- .../src/operators/contributionOperator.ts | 1 + .../src/operators/flattenOperator.ts | 1 + .../src/operators/prophetOperator.ts | 1 + .../src/operators/resampleOperator.ts | 1 + .../src/query/buildQueryContext.ts | 36 ++++--------------- .../EstimateQueryCostButton/index.tsx | 6 ---- .../src/SqlLab/components/SqlEditor/index.jsx | 3 -- .../components/SliceHeaderControls/index.tsx | 1 - .../FilterTitleContainer.tsx | 30 ---------------- .../components/TimeColumn/buildQuery.ts | 2 +- .../src/middleware/asyncEvent.ts | 6 ++-- .../src/utils/downloadAsImage.ts | 3 +- .../database/DatabaseModal/SqlAlchemyForm.tsx | 2 -- .../data/database/DatabaseModal/index.tsx | 1 - .../src/views/CRUD/data/query/QueryList.tsx | 2 +- .../CRUD/data/savedquery/SavedQueryList.tsx | 1 - 18 files changed, 18 insertions(+), 90 deletions(-) diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard/nativeFilters.test.ts b/superset-frontend/cypress-base/cypress/integration/dashboard/nativeFilters.test.ts index 01dc4b3495264..b409aa06d0a2e 100644 --- a/superset-frontend/cypress-base/cypress/integration/dashboard/nativeFilters.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/dashboard/nativeFilters.test.ts @@ -59,13 +59,6 @@ import { import { DASHBOARD_LIST } from '../dashboard_list/dashboard_list.helper'; import { CHART_LIST } from '../chart_list/chart_list.helper'; -const getTestTitle = ( - test: Mocha.Suite = (Cypress as any).mocha.getRunner().suite.ctx.test, -): string => - test.parent?.title - ? `${getTestTitle(test.parent)} -- ${test.title}` - : test.title; - // TODO: fix flaky init logic and re-enable const milliseconds = new Date().getTime(); const dashboard = `Test Dashboard${milliseconds}`; diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard/save.test.js b/superset-frontend/cypress-base/cypress/integration/dashboard/save.test.js index b0e9d1141cd30..3c815a222ce1e 100644 --- a/superset-frontend/cypress-base/cypress/integration/dashboard/save.test.js +++ b/superset-frontend/cypress-base/cypress/integration/dashboard/save.test.js @@ -36,7 +36,7 @@ describe('Dashboard save action', () => { beforeEach(() => { cy.login(); cy.visit(WORLD_HEALTH_DASHBOARD); - cy.get('#app').then(data => { + cy.get('#app').then(() => { cy.get('.dashboard-header-container').then(headerContainerElement => { const dashboardId = headerContainerElement.attr('data-test-id'); @@ -57,7 +57,7 @@ describe('Dashboard save action', () => { // change to what the title should be it('should save as new dashboard', () => { - cy.wait('@copyRequest').then(xhr => { + cy.wait('@copyRequest').then(() => { cy.get('[data-test="editable-title"]').then(element => { const dashboardTitle = element.attr('title'); expect(dashboardTitle).to.not.equal(`World Bank's Data`); diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/operators/contributionOperator.ts b/superset-frontend/packages/superset-ui-chart-controls/src/operators/contributionOperator.ts index 484117c44fcdb..39fa9c44337bc 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/operators/contributionOperator.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/operators/contributionOperator.ts @@ -19,6 +19,7 @@ import { PostProcessingContribution } from '@superset-ui/core'; import { PostProcessingFactory } from './types'; +/* eslint-disable @typescript-eslint/no-unused-vars */ export const contributionOperator: PostProcessingFactory = (formData, queryObject) => { if (formData.contributionMode) { diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/operators/flattenOperator.ts b/superset-frontend/packages/superset-ui-chart-controls/src/operators/flattenOperator.ts index 2fe732fc83d06..d5aa4ab186bfd 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/operators/flattenOperator.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/operators/flattenOperator.ts @@ -19,6 +19,7 @@ import { PostProcessingFlatten } from '@superset-ui/core'; import { PostProcessingFactory } from './types'; +/* eslint-disable @typescript-eslint/no-unused-vars */ export const flattenOperator: PostProcessingFactory = ( formData, queryObject, diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/operators/prophetOperator.ts b/superset-frontend/packages/superset-ui-chart-controls/src/operators/prophetOperator.ts index a55c8d3d9e48b..ff0fa0fb6544c 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/operators/prophetOperator.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/operators/prophetOperator.ts @@ -23,6 +23,7 @@ import { } from '@superset-ui/core'; import { PostProcessingFactory } from './types'; +/* eslint-disable @typescript-eslint/no-unused-vars */ export const prophetOperator: PostProcessingFactory = ( formData, queryObject, diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/operators/resampleOperator.ts b/superset-frontend/packages/superset-ui-chart-controls/src/operators/resampleOperator.ts index 2306ea38f924c..b157a054d138e 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/operators/resampleOperator.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/operators/resampleOperator.ts @@ -20,6 +20,7 @@ import { PostProcessingResample } from '@superset-ui/core'; import { PostProcessingFactory } from './types'; +/* eslint-disable @typescript-eslint/no-unused-vars */ export const resampleOperator: PostProcessingFactory = ( formData, queryObject, diff --git a/superset-frontend/packages/superset-ui-core/src/query/buildQueryContext.ts b/superset-frontend/packages/superset-ui-core/src/query/buildQueryContext.ts index ad35434cad09d..dbc1289c5460d 100644 --- a/superset-frontend/packages/superset-ui-core/src/query/buildQueryContext.ts +++ b/superset-frontend/packages/superset-ui-core/src/query/buildQueryContext.ts @@ -24,19 +24,7 @@ import { QueryContext, QueryObject } from './types/Query'; import { SetDataMaskHook } from '../chart'; import { JsonObject } from '../connection'; -const WRAP_IN_ARRAY = ( - baseQueryObject: QueryObject, - options?: { - extras?: { - cachedChanges?: any; - }; - ownState?: JsonObject; - hooks?: { - setDataMask: SetDataMaskHook; - setCachedChanges: (newChanges: any) => void; - }; - }, -) => [baseQueryObject]; +const WRAP_IN_ARRAY = (baseQueryObject: QueryObject) => [baseQueryObject]; export type BuildFinalQueryObjects = ( baseQueryObject: QueryObject, @@ -53,23 +41,11 @@ export default function buildQueryContext( } | BuildFinalQueryObjects, ): QueryContext { - const { - queryFields, - buildQuery = WRAP_IN_ARRAY, - hooks = {}, - ownState = {}, - } = typeof options === 'function' - ? { buildQuery: options, queryFields: {} } - : options || {}; - const queries = buildQuery(buildQueryObject(formData, queryFields), { - extras: {}, - ownState, - hooks: { - setDataMask: () => {}, - setCachedChanges: () => {}, - ...hooks, - }, - }); + const { queryFields, buildQuery = WRAP_IN_ARRAY } = + typeof options === 'function' + ? { buildQuery: options, queryFields: {} } + : options || {}; + const queries = buildQuery(buildQueryObject(formData, queryFields)); queries.forEach(query => { if (Array.isArray(query.post_processing)) { // eslint-disable-next-line no-param-reassign diff --git a/superset-frontend/src/SqlLab/components/EstimateQueryCostButton/index.tsx b/superset-frontend/src/SqlLab/components/EstimateQueryCostButton/index.tsx index 797c7504282ea..d7f2d7dd6dd43 100644 --- a/superset-frontend/src/SqlLab/components/EstimateQueryCostButton/index.tsx +++ b/superset-frontend/src/SqlLab/components/EstimateQueryCostButton/index.tsx @@ -26,9 +26,6 @@ import ModalTrigger from 'src/components/ModalTrigger'; import { EmptyWrapperType } from 'src/components/TableView/TableView'; interface EstimateQueryCostButtonProps { - dbId: number; - schema: string; - sql: string; getEstimate: Function; queryCostEstimate: Record; selectedText?: string; @@ -37,9 +34,6 @@ interface EstimateQueryCostButtonProps { } const EstimateQueryCostButton = ({ - dbId, - schema, - sql, getEstimate, queryCostEstimate = {}, selectedText, diff --git a/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx b/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx index a01ff33a969d1..d40ca65f2f665 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx +++ b/superset-frontend/src/SqlLab/components/SqlEditor/index.jsx @@ -686,9 +686,6 @@ class SqlEditor extends React.PureComponent { this.props.database.allows_cost_estimate && ( { diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FilterTitleContainer.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FilterTitleContainer.tsx index f5fe459e4b260..fe56777c4b08e 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FilterTitleContainer.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FilterTitleContainer.tsx @@ -146,36 +146,6 @@ const FilterTitleContainer = forwardRef( ); }; - const recursivelyRender = ( - elementId: string, - nodeList: Array<{ id: string; parentId: string | null }>, - rendered: Array, - ): React.ReactNode => { - const didAlreadyRender = rendered.indexOf(elementId) >= 0; - if (didAlreadyRender) { - return null; - } - let parent = null; - const element = nodeList.filter(el => el.id === elementId)[0]; - if (!element) { - return null; - } - - rendered.push(elementId); - if (element.parentId) { - parent = recursivelyRender(element.parentId, nodeList, rendered); - } - const children = nodeList - .filter(item => item.parentId === elementId) - .map(item => recursivelyRender(item.id, nodeList, rendered)); - return ( - <> - {parent} - {renderComponent(elementId)} - {children} - - ); - }; const renderFilterGroups = () => { const items: React.ReactNode[] = []; diff --git a/superset-frontend/src/filters/components/TimeColumn/buildQuery.ts b/superset-frontend/src/filters/components/TimeColumn/buildQuery.ts index 1cc839c4cb683..c271a697957e2 100644 --- a/superset-frontend/src/filters/components/TimeColumn/buildQuery.ts +++ b/superset-frontend/src/filters/components/TimeColumn/buildQuery.ts @@ -33,7 +33,7 @@ import { buildQueryContext, QueryFormData } from '@superset-ui/core'; * if a viz needs multiple different result sets. */ export default function buildQuery(formData: QueryFormData) { - return buildQueryContext(formData, baseQueryObject => [ + return buildQueryContext(formData, () => [ { result_type: 'columns', columns: [], diff --git a/superset-frontend/src/middleware/asyncEvent.ts b/superset-frontend/src/middleware/asyncEvent.ts index 9d252f99cc4d4..9ae4f90e479f4 100644 --- a/superset-frontend/src/middleware/asyncEvent.ts +++ b/superset-frontend/src/middleware/asyncEvent.ts @@ -193,13 +193,13 @@ const wsConnect = (): void => { if (lastReceivedEventId) url += `?last_id=${lastReceivedEventId}`; ws = new WebSocket(url); - ws.addEventListener('open', event => { + ws.addEventListener('open', () => { logging.log('WebSocket connected'); clearTimeout(wsConnectTimeout); wsConnectRetries = 0; }); - ws.addEventListener('close', event => { + ws.addEventListener('close', () => { wsConnectTimeout = setTimeout(() => { wsConnectRetries += 1; if (wsConnectRetries <= wsConnectMaxRetries) { @@ -211,7 +211,7 @@ const wsConnect = (): void => { }, wsConnectErrorDelay); }); - ws.addEventListener('error', event => { + ws.addEventListener('error', () => { // https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState if (ws.readyState < 2) ws.close(); }); diff --git a/superset-frontend/src/utils/downloadAsImage.ts b/superset-frontend/src/utils/downloadAsImage.ts index da9ca8b614b21..8c074bcdf2e69 100644 --- a/superset-frontend/src/utils/downloadAsImage.ts +++ b/superset-frontend/src/utils/downloadAsImage.ts @@ -17,7 +17,7 @@ * under the License. */ import { SyntheticEvent } from 'react'; -import domToImage, { Options } from 'dom-to-image'; +import domToImage from 'dom-to-image'; import kebabCase from 'lodash/kebabCase'; import { t } from '@superset-ui/core'; import { addWarningToast } from 'src/components/MessageToasts/actions'; @@ -50,7 +50,6 @@ const generateFileStem = (description: string, date = new Date()) => export default function downloadAsImage( selector: string, description: string, - domToImageOptions: Options = {}, isExactSelector = false, ) { return (event: SyntheticEvent) => { diff --git a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/SqlAlchemyForm.tsx b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/SqlAlchemyForm.tsx index 96a0bfef07cc2..454070b52a91a 100644 --- a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/SqlAlchemyForm.tsx +++ b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/SqlAlchemyForm.tsx @@ -29,14 +29,12 @@ const SqlAlchemyTab = ({ onInputChange, testConnection, conf, - isEditMode = false, testInProgress = false, }: { db: DatabaseObject | null; onInputChange: EventHandler>; testConnection: EventHandler>; conf: { SQLALCHEMY_DOCS_URL: string; SQLALCHEMY_DISPLAY_TEXT: string }; - isEditMode?: boolean; testInProgress?: boolean; }) => { let fallbackDocsUrl; diff --git a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.tsx b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.tsx index ca387a28a069f..7891e6b0e2ed9 100644 --- a/superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.tsx +++ b/superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.tsx @@ -1275,7 +1275,6 @@ const DatabaseModal: FunctionComponent = ({ } conf={conf} testConnection={testConnection} - isEditMode={isEditMode} testInProgress={testInProgress} /> {isDynamic(db?.backend || db?.engine) && !isEditMode && ( diff --git a/superset-frontend/src/views/CRUD/data/query/QueryList.tsx b/superset-frontend/src/views/CRUD/data/query/QueryList.tsx index da590f729a2b8..6ab71b8d43671 100644 --- a/superset-frontend/src/views/CRUD/data/query/QueryList.tsx +++ b/superset-frontend/src/views/CRUD/data/query/QueryList.tsx @@ -80,7 +80,7 @@ const StyledPopoverItem = styled.div` color: ${({ theme }) => theme.colors.grayscale.dark2}; `; -function QueryList({ addDangerToast, addSuccessToast }: QueryListProps) { +function QueryList({ addDangerToast }: QueryListProps) { const { state: { loading, resourceCount: queryCount, resourceCollection: queries }, fetchData, diff --git a/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx b/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx index d2dc6aff9c3f7..f3d58fe52cce8 100644 --- a/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx +++ b/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx @@ -90,7 +90,6 @@ const StyledPopoverItem = styled.div` function SavedQueryList({ addDangerToast, addSuccessToast, - user, }: SavedQueryListProps) { const { state: { From 1882c6d5cc397dfa2826266dfa100b7cfbe7bdd7 Mon Sep 17 00:00:00 2001 From: Atticus White Date: Wed, 15 Jun 2022 15:38:30 -0400 Subject: [PATCH 34/71] Remove cache warming documentation (#20269) The cache warming strategies perform unauthorized `GET` requests that are redirected to the login page #9597 --- docs/docs/installation/cache.mdx | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/docs/docs/installation/cache.mdx b/docs/docs/installation/cache.mdx index df2fc1471d29d..aaa8327451b8f 100644 --- a/docs/docs/installation/cache.mdx +++ b/docs/docs/installation/cache.mdx @@ -42,26 +42,6 @@ defined in `DATA_CACHE_CONFIG`. ## Celery beat -Superset has a Celery task that will periodically warm up the cache based on different strategies. -To use it, add the following to the `CELERYBEAT_SCHEDULE` section in `config.py`: - -```python -CELERYBEAT_SCHEDULE = { - 'cache-warmup-hourly': { - 'task': 'cache-warmup', - 'schedule': crontab(minute=0, hour='*'), # hourly - 'kwargs': { - 'strategy_name': 'top_n_dashboards', - 'top_n': 5, - 'since': '7 days ago', - }, - }, -} -``` - -This will cache all the charts in the top 5 most popular dashboards every hour. For other -strategies, check the `superset/tasks/cache.py` file. - ### Caching Thumbnails This is an optional feature that can be turned on by activating it’s feature flag on config: From 498987a1a0fa8af6d4c6375b91600ff4361b0e61 Mon Sep 17 00:00:00 2001 From: Reese <10563996+reesercollins@users.noreply.github.com> Date: Wed, 15 Jun 2022 16:28:13 -0400 Subject: [PATCH 35/71] Prevent dataset edit modal closing on click-away in edit mode (#20278) --- .../src/components/Datasource/DatasourceEditor.jsx | 3 +++ .../src/components/Datasource/DatasourceModal.tsx | 3 +++ superset-frontend/src/components/Modal/Modal.tsx | 1 + 3 files changed, 7 insertions(+) diff --git a/superset-frontend/src/components/Datasource/DatasourceEditor.jsx b/superset-frontend/src/components/Datasource/DatasourceEditor.jsx index afa47ea80b984..89ddcbdb81b42 100644 --- a/superset-frontend/src/components/Datasource/DatasourceEditor.jsx +++ b/superset-frontend/src/components/Datasource/DatasourceEditor.jsx @@ -518,10 +518,12 @@ const propTypes = { onChange: PropTypes.func, addSuccessToast: PropTypes.func.isRequired, addDangerToast: PropTypes.func.isRequired, + setIsEditing: PropTypes.func, }; const defaultProps = { onChange: () => {}, + setIsEditing: () => {}, }; function OwnersSelector({ datasource, onChange }) { @@ -629,6 +631,7 @@ class DatasourceEditor extends React.PureComponent { } onChangeEditMode() { + this.props.setIsEditing(!this.state.isEditMode); this.setState(prevState => ({ isEditMode: !prevState.isEditMode })); } diff --git a/superset-frontend/src/components/Datasource/DatasourceModal.tsx b/superset-frontend/src/components/Datasource/DatasourceModal.tsx index 49cb7ae5a4906..98f2e561db83f 100644 --- a/superset-frontend/src/components/Datasource/DatasourceModal.tsx +++ b/superset-frontend/src/components/Datasource/DatasourceModal.tsx @@ -84,6 +84,7 @@ const DatasourceModal: FunctionComponent = ({ const [currentDatasource, setCurrentDatasource] = useState(datasource); const [errors, setErrors] = useState([]); const [isSaving, setIsSaving] = useState(false); + const [isEditing, setIsEditing] = useState(false); const dialog = useRef(null); const [modal, contextHolder] = Modal.useModal(); @@ -193,6 +194,7 @@ const DatasourceModal: FunctionComponent = ({ {currentDatasource.table_name} } + maskClosable={!isEditing} footer={ <> {showLegacyDatasourceEditor && ( @@ -246,6 +248,7 @@ const DatasourceModal: FunctionComponent = ({ height={500} datasource={currentDatasource} onChange={onDatasourceChange} + setIsEditing={setIsEditing} /> {contextHolder} diff --git a/superset-frontend/src/components/Modal/Modal.tsx b/superset-frontend/src/components/Modal/Modal.tsx index c6d5b3ee0aac6..3bd21fef8075f 100644 --- a/superset-frontend/src/components/Modal/Modal.tsx +++ b/superset-frontend/src/components/Modal/Modal.tsx @@ -56,6 +56,7 @@ export interface ModalProps { draggable?: boolean; draggableConfig?: DraggableProps; destroyOnClose?: boolean; + maskClosable?: boolean; } interface StyledModalProps { From ccba5b2f69f8d9efb805e584de84f68afab1bb2c Mon Sep 17 00:00:00 2001 From: Elizabeth Thompson Date: Wed, 15 Jun 2022 16:27:55 -0700 Subject: [PATCH 36/71] chore: add action to welcome new users (#20401) * add action to welcome new users * fix typo --- .github/workflows/welcome-new-users.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/welcome-new-users.yml diff --git a/.github/workflows/welcome-new-users.yml b/.github/workflows/welcome-new-users.yml new file mode 100644 index 0000000000000..e55028af94461 --- /dev/null +++ b/.github/workflows/welcome-new-users.yml @@ -0,0 +1,25 @@ +name: Welcome New Contributor + +on: + pull_request_target: + types: [opened] + +jobs: + welcome: + runs-on: ubuntu-latest + permissions: + issues: write + + steps: + - name: Welcome Message + uses: actions/first-interaction@v1.0.0 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + pr-message: |- + Congrats on making your first PR and thank you for contributing to Superset! :tada: :heart: + We hope to see you in our [Slack](https://apache-superset.slack.com/) community too! + - name: First Time Label + uses: andymckay/labeler@master + with: + add-labels: "new:contributor" + repo-token: ${{ secrets.GITHUB_TOKEN }} From 16654034849505109b638fd2a784dfb377238a0e Mon Sep 17 00:00:00 2001 From: Stephen Liu <750188453@qq.com> Date: Thu, 16 Jun 2022 12:22:09 +0800 Subject: [PATCH 37/71] fix(plugin-chart-pivot-table): color weight of Conditional formatting metrics not work (#20396) * fix(plugin-chart-pivot-table): color weight of Conditional formatting metrics not work * fix: test --- .../src/utils/getColorFormatters.ts | 7 +- .../test/utils/getColorFormatters.test.ts | 99 ++++++++----------- 2 files changed, 45 insertions(+), 61 deletions(-) diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/utils/getColorFormatters.ts b/superset-frontend/packages/superset-ui-chart-controls/src/utils/getColorFormatters.ts index e15dc22d2b659..37729459f8dac 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/utils/getColorFormatters.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/utils/getColorFormatters.ts @@ -17,7 +17,7 @@ * under the License. */ import memoizeOne from 'memoize-one'; -import { DataRecord } from '@superset-ui/core'; +import { addAlpha, DataRecord } from '@superset-ui/core'; import { ColorFormatters, COMPARATOR, @@ -28,9 +28,6 @@ import { export const round = (num: number, precision = 0) => Number(`${Math.round(Number(`${num}e+${precision}`))}e-${precision}`); -export const rgbToRgba = (rgb: string, alpha: number) => - rgb.replace(/rgb/i, 'rgba').replace(/\)/i, `,${alpha})`); - const MIN_OPACITY_BOUNDED = 0.05; const MIN_OPACITY_UNBOUNDED = 0; const MAX_OPACITY = 1; @@ -174,7 +171,7 @@ export const getColorFunction = ( const compareResult = comparatorFunction(value, columnValues); if (compareResult === false) return undefined; const { cutoffValue, extremeValue } = compareResult; - return rgbToRgba( + return addAlpha( colorScheme, getOpacity(value, cutoffValue, extremeValue, minOpacity, maxOpacity), ); diff --git a/superset-frontend/packages/superset-ui-chart-controls/test/utils/getColorFormatters.test.ts b/superset-frontend/packages/superset-ui-chart-controls/test/utils/getColorFormatters.test.ts index bc69533c92555..051089f87cfc6 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/test/utils/getColorFormatters.test.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/test/utils/getColorFormatters.test.ts @@ -20,7 +20,6 @@ import { configure } from '@superset-ui/core'; import { COMPARATOR, getOpacity, - rgbToRgba, round, getColorFormatters, getColorFunction, @@ -54,25 +53,19 @@ describe('getOpacity', () => { }); }); -describe('rgba', () => { - it('returns correct rgba value', () => { - expect(rgbToRgba('rgb(255,0,0)', 0.5)).toEqual('rgba(255,0,0,0.5)'); - }); -}); - describe('getColorFunction()', () => { it('getColorFunction GREATER_THAN', () => { const colorFunction = getColorFunction( { operator: COMPARATOR.GREATER_THAN, targetValue: 50, - colorScheme: 'rgb(255,0,0)', + colorScheme: '#FF0000', column: 'count', }, countValues, ); expect(colorFunction(50)).toBeUndefined(); - expect(colorFunction(100)).toEqual('rgba(255,0,0,1)'); + expect(colorFunction(100)).toEqual('#FF0000FF'); }); it('getColorFunction LESS_THAN', () => { @@ -80,13 +73,13 @@ describe('getColorFunction()', () => { { operator: COMPARATOR.LESS_THAN, targetValue: 100, - colorScheme: 'rgb(255,0,0)', + colorScheme: '#FF0000', column: 'count', }, countValues, ); expect(colorFunction(100)).toBeUndefined(); - expect(colorFunction(50)).toEqual('rgba(255,0,0,1)'); + expect(colorFunction(50)).toEqual('#FF0000FF'); }); it('getColorFunction GREATER_OR_EQUAL', () => { @@ -94,13 +87,13 @@ describe('getColorFunction()', () => { { operator: COMPARATOR.GREATER_OR_EQUAL, targetValue: 50, - colorScheme: 'rgb(255,0,0)', + colorScheme: '#FF0000', column: 'count', }, countValues, ); - expect(colorFunction(50)).toEqual('rgba(255,0,0,0.05)'); - expect(colorFunction(100)).toEqual('rgba(255,0,0,1)'); + expect(colorFunction(50)).toEqual('#FF00000D'); + expect(colorFunction(100)).toEqual('#FF0000FF'); expect(colorFunction(0)).toBeUndefined(); }); @@ -109,13 +102,13 @@ describe('getColorFunction()', () => { { operator: COMPARATOR.LESS_OR_EQUAL, targetValue: 100, - colorScheme: 'rgb(255,0,0)', + colorScheme: '#FF0000', column: 'count', }, countValues, ); - expect(colorFunction(50)).toEqual('rgba(255,0,0,1)'); - expect(colorFunction(100)).toEqual('rgba(255,0,0,0.05)'); + expect(colorFunction(50)).toEqual('#FF0000FF'); + expect(colorFunction(100)).toEqual('#FF00000D'); expect(colorFunction(150)).toBeUndefined(); }); @@ -124,13 +117,13 @@ describe('getColorFunction()', () => { { operator: COMPARATOR.EQUAL, targetValue: 100, - colorScheme: 'rgb(255,0,0)', + colorScheme: '#FF0000', column: 'count', }, countValues, ); expect(colorFunction(50)).toBeUndefined(); - expect(colorFunction(100)).toEqual('rgba(255,0,0,1)'); + expect(colorFunction(100)).toEqual('#FF0000FF'); }); it('getColorFunction NOT_EQUAL', () => { @@ -138,27 +131,27 @@ describe('getColorFunction()', () => { { operator: COMPARATOR.NOT_EQUAL, targetValue: 60, - colorScheme: 'rgb(255,0,0)', + colorScheme: '#FF0000', column: 'count', }, countValues, ); expect(colorFunction(60)).toBeUndefined(); - expect(colorFunction(100)).toEqual('rgba(255,0,0,1)'); - expect(colorFunction(50)).toEqual('rgba(255,0,0,0.29)'); + expect(colorFunction(100)).toEqual('#FF0000FF'); + expect(colorFunction(50)).toEqual('#FF00004A'); colorFunction = getColorFunction( { operator: COMPARATOR.NOT_EQUAL, targetValue: 90, - colorScheme: 'rgb(255,0,0)', + colorScheme: '#FF0000', column: 'count', }, countValues, ); expect(colorFunction(90)).toBeUndefined(); - expect(colorFunction(100)).toEqual('rgba(255,0,0,0.29)'); - expect(colorFunction(50)).toEqual('rgba(255,0,0,1)'); + expect(colorFunction(100)).toEqual('#FF00004A'); + expect(colorFunction(50)).toEqual('#FF0000FF'); }); it('getColorFunction BETWEEN', () => { @@ -167,13 +160,13 @@ describe('getColorFunction()', () => { operator: COMPARATOR.BETWEEN, targetValueLeft: 75, targetValueRight: 125, - colorScheme: 'rgb(255,0,0)', + colorScheme: '#FF0000', column: 'count', }, countValues, ); expect(colorFunction(50)).toBeUndefined(); - expect(colorFunction(100)).toEqual('rgba(255,0,0,0.53)'); + expect(colorFunction(100)).toEqual('#FF000087'); }); it('getColorFunction BETWEEN_OR_EQUAL', () => { @@ -182,13 +175,13 @@ describe('getColorFunction()', () => { operator: COMPARATOR.BETWEEN_OR_EQUAL, targetValueLeft: 50, targetValueRight: 100, - colorScheme: 'rgb(255,0,0)', + colorScheme: '#FF0000', column: 'count', }, countValues, ); - expect(colorFunction(50)).toEqual('rgba(255,0,0,0.05)'); - expect(colorFunction(100)).toEqual('rgba(255,0,0,1)'); + expect(colorFunction(50)).toEqual('#FF00000D'); + expect(colorFunction(100)).toEqual('#FF0000FF'); expect(colorFunction(150)).toBeUndefined(); }); @@ -198,12 +191,12 @@ describe('getColorFunction()', () => { operator: COMPARATOR.BETWEEN_OR_LEFT_EQUAL, targetValueLeft: 50, targetValueRight: 100, - colorScheme: 'rgb(255,0,0)', + colorScheme: '#FF0000', column: 'count', }, countValues, ); - expect(colorFunction(50)).toEqual('rgba(255,0,0,0.05)'); + expect(colorFunction(50)).toEqual('#FF00000D'); expect(colorFunction(100)).toBeUndefined(); }); @@ -213,13 +206,13 @@ describe('getColorFunction()', () => { operator: COMPARATOR.BETWEEN_OR_RIGHT_EQUAL, targetValueLeft: 50, targetValueRight: 100, - colorScheme: 'rgb(255,0,0)', + colorScheme: '#FF0000', column: 'count', }, countValues, ); expect(colorFunction(50)).toBeUndefined(); - expect(colorFunction(100)).toEqual('rgba(255,0,0,1)'); + expect(colorFunction(100)).toEqual('#FF0000FF'); }); it('getColorFunction GREATER_THAN with target value undefined', () => { @@ -227,7 +220,7 @@ describe('getColorFunction()', () => { { operator: COMPARATOR.GREATER_THAN, targetValue: undefined, - colorScheme: 'rgb(255,0,0)', + colorScheme: '#FF0000', column: 'count', }, countValues, @@ -242,7 +235,7 @@ describe('getColorFunction()', () => { operator: COMPARATOR.BETWEEN, targetValueLeft: undefined, targetValueRight: 100, - colorScheme: 'rgb(255,0,0)', + colorScheme: '#FF0000', column: 'count', }, countValues, @@ -257,7 +250,7 @@ describe('getColorFunction()', () => { operator: COMPARATOR.BETWEEN, targetValueLeft: 50, targetValueRight: undefined, - colorScheme: 'rgb(255,0,0)', + colorScheme: '#FF0000', column: 'count', }, countValues, @@ -272,7 +265,7 @@ describe('getColorFunction()', () => { // @ts-ignore operator: 'unsupported operator', targetValue: 50, - colorScheme: 'rgb(255,0,0)', + colorScheme: '#FF0000', column: 'count', }, countValues, @@ -285,15 +278,15 @@ describe('getColorFunction()', () => { const colorFunction = getColorFunction( { operator: COMPARATOR.NONE, - colorScheme: 'rgb(255,0,0)', + colorScheme: '#FF0000', column: 'count', }, countValues, ); expect(colorFunction(20)).toEqual(undefined); - expect(colorFunction(50)).toEqual('rgba(255,0,0,0)'); - expect(colorFunction(75)).toEqual('rgba(255,0,0,0.5)'); - expect(colorFunction(100)).toEqual('rgba(255,0,0,1)'); + expect(colorFunction(50)).toEqual('#FF000000'); + expect(colorFunction(75)).toEqual('#FF000080'); + expect(colorFunction(100)).toEqual('#FF0000FF'); expect(colorFunction(120)).toEqual(undefined); }); @@ -302,7 +295,7 @@ describe('getColorFunction()', () => { { operator: undefined, targetValue: 150, - colorScheme: 'rgb(255,0,0)', + colorScheme: '#FF0000', column: 'count', }, countValues, @@ -332,26 +325,26 @@ describe('getColorFormatters()', () => { { operator: COMPARATOR.GREATER_THAN, targetValue: 50, - colorScheme: 'rgb(255,0,0)', + colorScheme: '#FF0000', column: 'count', }, { operator: COMPARATOR.LESS_THAN, targetValue: 300, - colorScheme: 'rgb(255,0,0)', + colorScheme: '#FF0000', column: 'sum', }, { operator: COMPARATOR.BETWEEN, targetValueLeft: 75, targetValueRight: 125, - colorScheme: 'rgb(255,0,0)', + colorScheme: '#FF0000', column: 'count', }, { operator: COMPARATOR.GREATER_THAN, targetValue: 150, - colorScheme: 'rgb(255,0,0)', + colorScheme: '#FF0000', column: undefined, }, ]; @@ -359,20 +352,14 @@ describe('getColorFormatters()', () => { expect(colorFormatters.length).toEqual(3); expect(colorFormatters[0].column).toEqual('count'); - expect(colorFormatters[0].getColorFromValue(100)).toEqual( - 'rgba(255,0,0,1)', - ); + expect(colorFormatters[0].getColorFromValue(100)).toEqual('#FF0000FF'); expect(colorFormatters[1].column).toEqual('sum'); - expect(colorFormatters[1].getColorFromValue(200)).toEqual( - 'rgba(255,0,0,1)', - ); + expect(colorFormatters[1].getColorFromValue(200)).toEqual('#FF0000FF'); expect(colorFormatters[1].getColorFromValue(400)).toBeUndefined(); expect(colorFormatters[2].column).toEqual('count'); - expect(colorFormatters[2].getColorFromValue(100)).toEqual( - 'rgba(255,0,0,0.53)', - ); + expect(colorFormatters[2].getColorFromValue(100)).toEqual('#FF000087'); }); it('undefined column config', () => { From c959d92dd17499e3fb7a0f4f02f3781516f3d3e6 Mon Sep 17 00:00:00 2001 From: Kamil Gabryjelski Date: Thu, 16 Jun 2022 13:58:58 +0200 Subject: [PATCH 38/71] feat(plugin-chart-echarts): Support stacking negative and positive values (#20408) --- superset-frontend/package-lock.json | 32 +++++++++---------- .../plugins/plugin-chart-echarts/package.json | 2 +- .../src/Timeseries/transformers.ts | 1 + 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index ce70b2368a3ab..0ec9482e2d2b3 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -26530,12 +26530,12 @@ } }, "node_modules/echarts": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.3.2.tgz", - "integrity": "sha512-LWCt7ohOKdJqyiBJ0OGBmE9szLdfA9sGcsMEi+GGoc6+Xo75C+BkcT/6NNGRHAWtnQl2fNow05AQjznpap28TQ==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.3.3.tgz", + "integrity": "sha512-BRw2serInRwO5SIwRviZ6Xgm5Lb7irgz+sLiFMmy/HOaf4SQ+7oYqxKzRHAKp4xHQ05AuHw1xvoQWJjDQq/FGw==", "dependencies": { "tslib": "2.3.0", - "zrender": "5.3.1" + "zrender": "5.3.2" } }, "node_modules/echarts/node_modules/tslib": { @@ -54238,9 +54238,9 @@ } }, "node_modules/zrender": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.3.1.tgz", - "integrity": "sha512-7olqIjy0gWfznKr6vgfnGBk7y4UtdMvdwFmK92vVQsQeDPyzkHW1OlrLEKg6GHz1W5ePf0FeN1q2vkl/HFqhXw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.3.2.tgz", + "integrity": "sha512-8IiYdfwHj2rx0UeIGZGGU4WEVSDEdeVCaIg/fomejg1Xu6OifAL1GVzIPHg2D+MyUkbNgPWji90t0a8IDk+39w==", "dependencies": { "tslib": "2.3.0" } @@ -55365,7 +55365,7 @@ "license": "Apache-2.0", "dependencies": { "d3-array": "^1.2.0", - "echarts": "^5.3.2", + "echarts": "^5.3.3", "lodash": "^4.17.15", "moment": "^2.26.0" }, @@ -68177,7 +68177,7 @@ "version": "file:plugins/plugin-chart-echarts", "requires": { "d3-array": "^1.2.0", - "echarts": "^5.3.2", + "echarts": "^5.3.3", "lodash": "^4.17.15", "moment": "^2.26.0" } @@ -76574,12 +76574,12 @@ } }, "echarts": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.3.2.tgz", - "integrity": "sha512-LWCt7ohOKdJqyiBJ0OGBmE9szLdfA9sGcsMEi+GGoc6+Xo75C+BkcT/6NNGRHAWtnQl2fNow05AQjznpap28TQ==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.3.3.tgz", + "integrity": "sha512-BRw2serInRwO5SIwRviZ6Xgm5Lb7irgz+sLiFMmy/HOaf4SQ+7oYqxKzRHAKp4xHQ05AuHw1xvoQWJjDQq/FGw==", "requires": { "tslib": "2.3.0", - "zrender": "5.3.1" + "zrender": "5.3.2" }, "dependencies": { "tslib": { @@ -98014,9 +98014,9 @@ } }, "zrender": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.3.1.tgz", - "integrity": "sha512-7olqIjy0gWfznKr6vgfnGBk7y4UtdMvdwFmK92vVQsQeDPyzkHW1OlrLEKg6GHz1W5ePf0FeN1q2vkl/HFqhXw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.3.2.tgz", + "integrity": "sha512-8IiYdfwHj2rx0UeIGZGGU4WEVSDEdeVCaIg/fomejg1Xu6OifAL1GVzIPHg2D+MyUkbNgPWji90t0a8IDk+39w==", "requires": { "tslib": "2.3.0" }, diff --git a/superset-frontend/plugins/plugin-chart-echarts/package.json b/superset-frontend/plugins/plugin-chart-echarts/package.json index 0c5cbcf595ae0..f37fbd95d919c 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/package.json +++ b/superset-frontend/plugins/plugin-chart-echarts/package.json @@ -27,7 +27,7 @@ }, "dependencies": { "d3-array": "^1.2.0", - "echarts": "^5.3.2", + "echarts": "^5.3.3", "lodash": "^4.17.15", "moment": "^2.26.0" }, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts index 93565de46c278..bd15cbf099b93 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformers.ts @@ -204,6 +204,7 @@ export function transformSeries( ? seriesType : undefined, stack: stackId, + stackStrategy: 'all', lineStyle, areaStyle: area || forecastSeries.type === ForecastSeriesEnum.ForecastUpper From 11d94ce56c472c0c516c4ce3b2ced03890e5d0ba Mon Sep 17 00:00:00 2001 From: chuancy <3195234362@qq.com> Date: Thu, 16 Jun 2022 22:27:53 +0800 Subject: [PATCH 39/71] Chinese translation and English translation do not match (#20405) --- .../translations/zh/LC_MESSAGES/messages.po | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/superset/translations/zh/LC_MESSAGES/messages.po b/superset/translations/zh/LC_MESSAGES/messages.po index 7b2570f7921ad..ea3762b1c6c91 100644 --- a/superset/translations/zh/LC_MESSAGES/messages.po +++ b/superset/translations/zh/LC_MESSAGES/messages.po @@ -3071,7 +3071,7 @@ msgstr "字段作为数据文件的行标签使用。如果没有索引字段, #: superset/views/database/forms.py:385 #, fuzzy msgid "Columnar File" -msgstr "列" +msgstr "列式存储文件" #: superset/views/database/views.py:550 #, fuzzy, python-format @@ -3079,13 +3079,13 @@ msgid "" "Columnar file \"%(columnar_filename)s\" uploaded to table " "\"%(table_name)s\" in database \"%(db_name)s\"" msgstr "" -"Excel 文件 \"%(excel_filename)s\" 上传到数据库 \"%(db_name)s\" 中的表 " +"Excel 文件 \"%(columnar_filename)s\" 上传到数据库 \"%(db_name)s\" 中的表 " "\"%(table_name)s\"" #: superset/views/database/views.py:414 #, fuzzy msgid "Columnar to Database configuration" -msgstr "Excel 到数据库配置" +msgstr "列式存储文件到数据库配置" #: superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/dndControls.tsx:55 #: superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx:214 @@ -10315,12 +10315,12 @@ msgstr "搜索指标和列" #: superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx:703 #, fuzzy msgid "Search all charts" -msgstr "所有图表" +msgstr "搜索所有图表" #: superset-frontend/src/components/OmniContainer/index.tsx:102 #, fuzzy msgid "Search all dashboards" -msgstr "刷新看板" +msgstr "搜索所有看板" #: superset-frontend/src/explore/components/controls/FilterBoxItemControl/index.jsx:232 #: superset-frontend/src/filters/components/Select/controlPanel.ts:137 @@ -11522,7 +11522,7 @@ msgstr "报告计划意外错误。" #: superset-frontend/src/views/CRUD/data/database/DatabaseModal/index.tsx:670 #, fuzzy msgid "Supported databases" -msgstr "删除数据库" +msgstr "已支持数据库" #: superset-frontend/plugins/legacy-plugin-chart-sankey/src/index.js:34 msgid "Survey Responses" @@ -12324,14 +12324,14 @@ msgstr "详细提示显示了该时间点的所有序列的列表。" msgid "" "The schema \"%(schema)s\" does not exist. A valid schema must be used to " "run this query." -msgstr "表 \"%(table_name)s\" 不存在。必须使用有效的表来运行此查询。" +msgstr "表 \"%(schema)s\" 不存在。必须使用有效的表来运行此查询。" #: superset/db_engine_specs/presto.py:187 #, fuzzy, python-format msgid "" "The schema \"%(schema_name)s\" does not exist. A valid schema must be " "used to run this query." -msgstr "表 \"%(table_name)s\" 不存在。必须使用有效的表来运行此查询。" +msgstr "表 \"%(schema_name)s\" 不存在。必须使用有效的表来运行此查询。" #: superset/errors.py:111 #, fuzzy @@ -12355,7 +12355,7 @@ msgstr "" msgid "" "The table \"%(table)s\" does not exist. A valid table must be used to run" " this query." -msgstr "表 \"%(table_name)s\" 不存在。必须使用有效的表来运行此查询。" +msgstr "表 \"%(table)s\" 不存在。必须使用有效的表来运行此查询。" #: superset/db_engine_specs/presto.py:179 #, python-format @@ -13641,10 +13641,10 @@ msgstr "上传" #: superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm/EncryptedField.tsx:134 #, fuzzy msgid "Upload Credentials" -msgstr "上传Excel" +msgstr "上传验证文件" #: superset/initialization/__init__.py:400 -msgid "Upload Excel" +msgid "Upload Excel file to database" msgstr "上传Excel" #: superset-frontend/src/views/CRUD/data/database/DatabaseModal/DatabaseConnectionForm/EncryptedField.tsx:102 @@ -13652,12 +13652,12 @@ msgid "Upload JSON file" msgstr "" #: superset/initialization/__init__.py:369 -msgid "Upload a CSV" +msgid "Upload CSV to database" msgstr "上传CSV文件" #: superset/initialization/__init__.py:383 msgid "Upload a Columnar File" -msgstr "" +msgstr "上传列式存储文件" #: superset-frontend/plugins/legacy-plugin-chart-rose/src/controlPanel.tsx:112 #, fuzzy @@ -14765,7 +14765,7 @@ msgid "" "\"%(columnar_table.table)s\" and in the schema field: " "\"%(columnar_table.schema)s\". Please remove one" msgstr "" -"不能同时在表名 \"%(excel_table.table)s\" 和schema字段 \"%(excel_table.schema)s\" " +"不能同时在表名 \"%(columnar_table.table)s\" 和schema字段 \"%(columnar_table.schema)s\" " "中指定命名空间。请删除一个。" #: superset/views/database/views.py:146 @@ -15572,4 +15572,4 @@ msgstr "年" #: superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx:42 msgid "yellow" -msgstr "" +msgstr "黄色" From 467d8ef89ea11dcfc4cd67d88a514886254820d2 Mon Sep 17 00:00:00 2001 From: Cody Leff Date: Thu, 16 Jun 2022 10:18:59 -0600 Subject: [PATCH 40/71] docs: add Matomo tracking to docs (#20398) * Apply Prettier formatting. * Add Matomo script to Docusaurus config. * Add Apache license to Matomo script. --- docs/docusaurus.config.js | 17 ++++++++++------- docs/static/script/matomo.js | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 docs/static/script/matomo.js diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 7c86ff067cd0f..2934afa62db9d 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -37,11 +37,14 @@ const config = { projectName: 'superset', // Usually your repo name. themes: ['@saucelabs/theme-github-codeblock'], plugins: [ - ["docusaurus-plugin-less", { - lessOptions: { - javascriptEnabled: true, - } - }], + [ + 'docusaurus-plugin-less', + { + lessOptions: { + javascriptEnabled: true, + }, + }, + ], [ '@docusaurus/plugin-client-redirects', { @@ -229,8 +232,7 @@ const config = { }, footer: { style: 'dark', - links: [ - ], + links: [], copyright: `Copyright © ${new Date().getFullYear()}, The Apache Software Foundation, Licensed under the Apache License.
@@ -249,6 +251,7 @@ const config = { darkTheme: darkCodeTheme, }, }), + scripts: ['/script/matomo.js'], }; module.exports = config; diff --git a/docs/static/script/matomo.js b/docs/static/script/matomo.js new file mode 100644 index 0000000000000..4af7a4e85dac4 --- /dev/null +++ b/docs/static/script/matomo.js @@ -0,0 +1,36 @@ +/** + * 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. + */ + +var _paq = (window._paq = window._paq || []); +/* tracker methods like "setCustomDimension" should be called before "trackPageView" */ +/* We explicitly disable cookie tracking to avoid privacy issues */ +_paq.push(['disableCookies']); +_paq.push(['trackPageView']); +_paq.push(['enableLinkTracking']); +(function () { + var u = 'https://analytics.apache.org/'; + _paq.push(['setTrackerUrl', u + 'matomo.php']); + _paq.push(['setSiteId', '22']); + var d = document, + g = d.createElement('script'), + s = d.getElementsByTagName('script')[0]; + g.async = true; + g.src = u + 'matomo.js'; + s.parentNode.insertBefore(g, s); +})(); From a8a6b732e9fb37ac2f4de953d099ce6a4937e17d Mon Sep 17 00:00:00 2001 From: Smart-Codi Date: Thu, 16 Jun 2022 19:55:14 +0100 Subject: [PATCH 41/71] adding extra metrics after chart configuration (#20410) --- superset-frontend/src/explore/reducers/exploreReducer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/superset-frontend/src/explore/reducers/exploreReducer.js b/superset-frontend/src/explore/reducers/exploreReducer.js index 818bb31c8c381..87c3aa9b0a6ee 100644 --- a/superset-frontend/src/explore/reducers/exploreReducer.js +++ b/superset-frontend/src/explore/reducers/exploreReducer.js @@ -141,8 +141,8 @@ export default function exploreReducer(state = {}, action) { if (controlName === 'metrics' && old_metrics_data && new_column_config) { value.forEach((item, index) => { if ( - item.label !== old_metrics_data[index].label && - !!new_column_config[old_metrics_data[index].label] + item?.label !== old_metrics_data[index]?.label && + !!new_column_config[old_metrics_data[index]?.label] ) { new_column_config[item.label] = new_column_config[old_metrics_data[index].label]; From fadf0ec5ad74dc3f21eb099b4defe5ed5ec0b449 Mon Sep 17 00:00:00 2001 From: Cody Leff Date: Thu, 16 Jun 2022 15:05:37 -0600 Subject: [PATCH 42/71] Update documentation on updating documentation. (#20400) --- CONTRIBUTING.md | 34 +++++++++++++++++++++------------- docs/README.md | 34 +--------------------------------- 2 files changed, 22 insertions(+), 46 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 653cfe41a5f0c..b94aeea569427 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -160,7 +160,7 @@ Look through the GitHub issues. Issues tagged with Superset could always use better documentation, whether as part of the official Superset docs, -in docstrings, `docs/*.rst` or even on the web as blog posts or +in docstrings, or even on the web as blog posts or articles. See [Documentation](#documentation) for more details. ### Add Translations @@ -388,23 +388,30 @@ cd superset The latest documentation and tutorial are available at https://superset.apache.org/. -The site is written using the Gatsby framework and docz for the -documentation subsection. Find out more about it in `docs/README.md` +The documentation site is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator, the source for which resides in `./docs`. -#### Images +#### Local Development -If you're adding new images to the documentation, you'll notice that the images -referenced in the rst, e.g. +To set up a local development environment with hot reloading for the documentation site: - .. image:: _static/images/tutorial/tutorial_01_sources_database.png +```shell +cd docs +yarn install # Installs NPM dependencies +yarn start # Starts development server at http://localhost:3000 +``` + +#### Build + +To create and serve a production build of the documentation site: -aren't actually stored in that directory. Instead, you should add and commit -images (and any other static assets) to the `superset-frontend/src/assets/images` directory. -When the docs are deployed to https://superset.apache.org/, images -are copied from there to the `_static/images` directory, just like they're referenced -in the docs. +```shell +yarn build +yarn serve +``` + +#### Deployment -For example, the image referenced above actually lives in `superset-frontend/src/assets/images/tutorial`. Since the image is moved during the documentation build process, the docs reference the image in `_static/images/tutorial` instead. +Commits to `master` trigger a rebuild and redeploy of the documentation site. Submit pull requests that modify the documention with the `docs:` prefix. ### Flask server @@ -1064,6 +1071,7 @@ LANGUAGES = { ``` This script will + 1. update the template file `superset/translations/messages.pot` with current application strings. 2. update language files with the new extracted strings. diff --git a/docs/README.md b/docs/README.md index f4a122ba2f5b1..bd31f144c8573 100644 --- a/docs/README.md +++ b/docs/README.md @@ -17,36 +17,4 @@ specific language governing permissions and limitations under the License. --> -# Website - -This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. - -### Installation - -``` -$ yarn install -``` - -### Local Development - -``` -$ yarn start -``` - -This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. - -### Build - -``` -$ yarn build -``` - -This command generates static content into the `build` directory and can be served using any static contents hosting service. - -### Deployment - -``` -$ GIT_USER= USE_SSH=true yarn deploy -``` - -If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. +This is the public documentation site for Superset, built using [Docusaurus 2](https://docusaurus.io/). See [CONTRIBUTING.md](../CONTRIBUTING.md#documentation)` for documentation on contributing to documentation. From 12436e47c9e11af3769f692988cb9c8c724c272e Mon Sep 17 00:00:00 2001 From: Elizabeth Thompson Date: Thu, 16 Jun 2022 15:51:17 -0700 Subject: [PATCH 43/71] fix key error on permalink fetch for old permalinks (#20414) --- superset/explore/permalink/commands/get.py | 9 +- superset/explore/permalink/types.py | 6 +- .../explore/permalink/commands_tests.py | 172 ++++++++++++++++++ 3 files changed, 184 insertions(+), 3 deletions(-) create mode 100644 tests/integration_tests/explore/permalink/commands_tests.py diff --git a/superset/explore/permalink/commands/get.py b/superset/explore/permalink/commands/get.py index f75df69d7a63e..ca4fe8c74fa2f 100644 --- a/superset/explore/permalink/commands/get.py +++ b/superset/explore/permalink/commands/get.py @@ -48,8 +48,13 @@ def run(self) -> Optional[ExplorePermalinkValue]: ).run() if value: chart_id: Optional[int] = value.get("chartId") - datasource_id: int = value["datasourceId"] - datasource_type = DatasourceType(value["datasourceType"]) + # keep this backward compatible for old permalinks + datasource_id: int = ( + value.get("datasourceId") or value.get("datasetId") or 0 + ) + datasource_type = DatasourceType( + value.get("datasourceType", DatasourceType.TABLE) + ) check_chart_access(datasource_id, chart_id, self.actor, datasource_type) return value return None diff --git a/superset/explore/permalink/types.py b/superset/explore/permalink/types.py index b90b4d760d4d0..393f0ed8d5890 100644 --- a/superset/explore/permalink/types.py +++ b/superset/explore/permalink/types.py @@ -24,7 +24,11 @@ class ExplorePermalinkState(TypedDict, total=False): class ExplorePermalinkValue(TypedDict): chartId: Optional[int] - datasourceId: int + # either datasetId or datasourceId is required + # TODO: deprecated - datasetId is deprecated + # and should be removed in next major release + datasetId: Optional[int] + datasourceId: Optional[int] datasourceType: str datasource: str state: ExplorePermalinkState diff --git a/tests/integration_tests/explore/permalink/commands_tests.py b/tests/integration_tests/explore/permalink/commands_tests.py new file mode 100644 index 0000000000000..2bb44bb068edd --- /dev/null +++ b/tests/integration_tests/explore/permalink/commands_tests.py @@ -0,0 +1,172 @@ +# 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 json +from unittest.mock import patch + +import pytest + +from superset import app, db, security, security_manager +from superset.commands.exceptions import DatasourceTypeInvalidError +from superset.connectors.sqla.models import SqlaTable +from superset.explore.form_data.commands.parameters import CommandParameters +from superset.explore.permalink.commands.create import CreateExplorePermalinkCommand +from superset.explore.permalink.commands.get import GetExplorePermalinkCommand +from superset.key_value.utils import decode_permalink_id +from superset.models.slice import Slice +from superset.models.sql_lab import Query +from superset.utils.core import DatasourceType, get_example_default_schema +from superset.utils.database import get_example_database +from tests.integration_tests.base_tests import SupersetTestCase + + +class TestCreatePermalinkDataCommand(SupersetTestCase): + @pytest.fixture() + def create_dataset(self): + with self.create_app().app_context(): + dataset = SqlaTable( + table_name="dummy_sql_table", + database=get_example_database(), + schema=get_example_default_schema(), + sql="select 123 as intcol, 'abc' as strcol", + ) + session = db.session + session.add(dataset) + session.commit() + + yield dataset + + # rollback + session.delete(dataset) + session.commit() + + @pytest.fixture() + def create_slice(self): + with self.create_app().app_context(): + session = db.session + dataset = ( + session.query(SqlaTable).filter_by(table_name="dummy_sql_table").first() + ) + slice = Slice( + datasource_id=dataset.id, + datasource_type=DatasourceType.TABLE, + datasource_name="tmp_perm_table", + slice_name="slice_name", + ) + + session.add(slice) + session.commit() + + yield slice + + # rollback + session.delete(slice) + session.commit() + + @pytest.fixture() + def create_query(self): + with self.create_app().app_context(): + session = db.session + + query = Query( + sql="select 1 as foo;", + client_id="sldkfjlk", + database=get_example_database(), + ) + + session.add(query) + session.commit() + + yield query + + # rollback + session.delete(query) + session.commit() + + @patch("superset.security.manager.g") + @pytest.mark.usefixtures("create_dataset", "create_slice") + def test_create_permalink_command(self, mock_g): + mock_g.user = security_manager.find_user("admin") + + dataset = ( + db.session.query(SqlaTable).filter_by(table_name="dummy_sql_table").first() + ) + slice = db.session.query(Slice).filter_by(slice_name="slice_name").first() + + datasource = f"{dataset.id}__{DatasourceType.TABLE}" + command = CreateExplorePermalinkCommand( + mock_g.user, {"formData": {"datasource": datasource, "slice_id": slice.id}} + ) + + assert isinstance(command.run(), str) + + @patch("superset.security.manager.g") + @pytest.mark.usefixtures("create_dataset", "create_slice") + def test_get_permalink_command(self, mock_g): + mock_g.user = security_manager.find_user("admin") + app.config["EXPLORE_FORM_DATA_CACHE_CONFIG"] = { + "REFRESH_TIMEOUT_ON_RETRIEVAL": True + } + + dataset = ( + db.session.query(SqlaTable).filter_by(table_name="dummy_sql_table").first() + ) + slice = db.session.query(Slice).filter_by(slice_name="slice_name").first() + + datasource = f"{dataset.id}__{DatasourceType.TABLE}" + + key = CreateExplorePermalinkCommand( + mock_g.user, {"formData": {"datasource": datasource, "slice_id": slice.id}} + ).run() + + get_command = GetExplorePermalinkCommand(mock_g.user, key) + cache_data = get_command.run() + + assert cache_data.get("datasource") == datasource + + @patch("superset.security.manager.g") + @patch("superset.key_value.commands.get.GetKeyValueCommand.run") + @patch("superset.explore.permalink.commands.get.decode_permalink_id") + @pytest.mark.usefixtures("create_dataset", "create_slice") + def test_get_permalink_command_with_old_dataset_key( + self, decode_id_mock, get_kv_command_mock, mock_g + ): + mock_g.user = security_manager.find_user("admin") + app.config["EXPLORE_FORM_DATA_CACHE_CONFIG"] = { + "REFRESH_TIMEOUT_ON_RETRIEVAL": True + } + + dataset = ( + db.session.query(SqlaTable).filter_by(table_name="dummy_sql_table").first() + ) + slice = db.session.query(Slice).filter_by(slice_name="slice_name").first() + + datasource_string = f"{dataset.id}__{DatasourceType.TABLE}" + + decode_id_mock.return_value = "123456" + get_kv_command_mock.return_value = { + "chartId": slice.id, + "datasetId": dataset.id, + "datasource": datasource_string, + "state": { + "formData": {"datasource": datasource_string, "slice_id": slice.id} + }, + } + get_command = GetExplorePermalinkCommand(mock_g.user, "thisisallmocked") + cache_data = get_command.run() + + assert cache_data.get("datasource") == datasource_string From 998624b1a5a498343bd7f37b5ca80402ba08e305 Mon Sep 17 00:00:00 2001 From: Beto Dealmeida Date: Thu, 16 Jun 2022 15:53:59 -0700 Subject: [PATCH 44/71] feat: allow setting db UUID (#20412) * WIP * feat: allow passing UUID when creating a DB * Test * Fix field --- superset/databases/schemas.py | 1 + tests/unit_tests/conftest.py | 20 +++++++ tests/unit_tests/databases/api_test.py | 53 +++++++++++++++++ tests/unit_tests/importexport/api_test.py | 70 ++++++++++------------- 4 files changed, 104 insertions(+), 40 deletions(-) create mode 100644 tests/unit_tests/databases/api_test.py diff --git a/superset/databases/schemas.py b/superset/databases/schemas.py index cdce5578cd7df..bd30ad4ec5bd1 100644 --- a/superset/databases/schemas.py +++ b/superset/databases/schemas.py @@ -397,6 +397,7 @@ class Meta: # pylint: disable=too-few-public-methods ) is_managed_externally = fields.Boolean(allow_none=True, default=False) external_url = fields.String(allow_none=True) + uuid = fields.String(required=False) class DatabasePutSchema(Schema, DatabaseParametersSchemaMixin): diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index 86fb0127b84f3..4560617d4bcb8 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -25,6 +25,7 @@ from sqlalchemy.orm import sessionmaker from sqlalchemy.orm.session import Session +from superset import security_manager from superset.app import SupersetApp from superset.extensions import appbuilder from superset.initialization import SupersetAppInitializer @@ -69,6 +70,8 @@ def app() -> Iterator[SupersetApp]: app.config.from_object("superset.config") app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite://" + app.config["WTF_CSRF_ENABLED"] = False + app.config["PREVENT_UNSAFE_DB_CONNECTIONS"] = False app.config["TESTING"] = True # ``superset.extensions.appbuilder`` is a singleton, and won't rebuild the @@ -101,3 +104,20 @@ def app_context(app: SupersetApp) -> Iterator[None]: """ with app.app_context(): yield + + +@pytest.fixture +def full_api_access(mocker: MockFixture) -> Iterator[None]: + """ + Allow full access to the API. + + TODO (betodealmeida): we should replace this with user-fixtures, eg, ``admin`` or + ``gamma``, so that we have granular access to the APIs. + """ + mocker.patch( + "flask_appbuilder.security.decorators.verify_jwt_in_request", + return_value=True, + ) + mocker.patch.object(security_manager, "has_access", return_value=True) + + yield diff --git a/tests/unit_tests/databases/api_test.py b/tests/unit_tests/databases/api_test.py new file mode 100644 index 0000000000000..f121b799fda67 --- /dev/null +++ b/tests/unit_tests/databases/api_test.py @@ -0,0 +1,53 @@ +# 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. + +# pylint: disable=unused-argument, import-outside-toplevel + +from typing import Any +from uuid import UUID + +from pytest_mock import MockFixture +from sqlalchemy.orm.session import Session + + +def test_post_with_uuid( + mocker: MockFixture, + app_context: None, + session: Session, + client: Any, + full_api_access: None, +) -> None: + """ + Test that we can set the database UUID when creating it. + """ + from superset.models.core import Database + + # create table for databases + Database.metadata.create_all(session.get_bind()) # pylint: disable=no-member + + response = client.post( + "/api/v1/database/", + json={ + "database_name": "my_db", + "sqlalchemy_uri": "sqlite://", + "uuid": "7c1b7880-a59d-47cd-8bf1-f1eb8d2863cb", + }, + ) + assert response.status_code == 201 + + database = session.query(Database).one() + assert database.uuid == UUID("7c1b7880-a59d-47cd-8bf1-f1eb8d2863cb") diff --git a/tests/unit_tests/importexport/api_test.py b/tests/unit_tests/importexport/api_test.py index e5dee975d8cd8..9c8c740255783 100644 --- a/tests/unit_tests/importexport/api_test.py +++ b/tests/unit_tests/importexport/api_test.py @@ -27,18 +27,16 @@ from superset import security_manager -def test_export_assets(mocker: MockFixture, client: Any) -> None: +def test_export_assets( + mocker: MockFixture, + client: Any, + full_api_access: None, +) -> None: """ Test exporting assets. """ from superset.commands.importers.v1.utils import get_contents_from_bundle - # grant access - mocker.patch( - "flask_appbuilder.security.decorators.verify_jwt_in_request", return_value=True - ) - mocker.patch.object(security_manager, "has_access", return_value=True) - mocked_contents = [ ( "metadata.yaml", @@ -62,16 +60,14 @@ def test_export_assets(mocker: MockFixture, client: Any) -> None: assert contents == dict(mocked_contents) -def test_import_assets(mocker: MockFixture, client: Any) -> None: +def test_import_assets( + mocker: MockFixture, + client: Any, + full_api_access: None, +) -> None: """ Test importing assets. """ - # grant access - mocker.patch( - "flask_appbuilder.security.decorators.verify_jwt_in_request", return_value=True - ) - mocker.patch.object(security_manager, "has_access", return_value=True) - mocked_contents = { "metadata.yaml": ( "version: 1.0.0\ntype: assets\ntimestamp: '2022-01-01T00:00:00+00:00'\n" @@ -105,16 +101,14 @@ def test_import_assets(mocker: MockFixture, client: Any) -> None: ImportAssetsCommand.assert_called_with(mocked_contents, passwords=passwords) -def test_import_assets_not_zip(mocker: MockFixture, client: Any) -> None: +def test_import_assets_not_zip( + mocker: MockFixture, + client: Any, + full_api_access: None, +) -> None: """ Test error message when the upload is not a ZIP file. """ - # grant access - mocker.patch( - "flask_appbuilder.security.decorators.verify_jwt_in_request", return_value=True - ) - mocker.patch.object(security_manager, "has_access", return_value=True) - buf = BytesIO(b"definitely_not_a_zip_file") form_data = { "bundle": (buf, "broken.txt"), @@ -145,14 +139,14 @@ def test_import_assets_not_zip(mocker: MockFixture, client: Any) -> None: } -def test_import_assets_no_form_data(mocker: MockFixture, client: Any) -> None: +def test_import_assets_no_form_data( + mocker: MockFixture, + client: Any, + full_api_access: None, +) -> None: """ Test error message when the upload has no form data. """ - # grant access - mocker.patch( - "flask_appbuilder.security.decorators.verify_jwt_in_request", return_value=True - ) mocker.patch.object(security_manager, "has_access", return_value=True) response = client.post("/api/v1/assets/import/", data="some_content") @@ -179,16 +173,14 @@ def test_import_assets_no_form_data(mocker: MockFixture, client: Any) -> None: } -def test_import_assets_incorrect_form_data(mocker: MockFixture, client: Any) -> None: +def test_import_assets_incorrect_form_data( + mocker: MockFixture, + client: Any, + full_api_access: None, +) -> None: """ Test error message when the upload form data has the wrong key. """ - # grant access - mocker.patch( - "flask_appbuilder.security.decorators.verify_jwt_in_request", return_value=True - ) - mocker.patch.object(security_manager, "has_access", return_value=True) - buf = BytesIO(b"definitely_not_a_zip_file") form_data = { "wrong": (buf, "broken.txt"), @@ -200,16 +192,14 @@ def test_import_assets_incorrect_form_data(mocker: MockFixture, client: Any) -> assert response.json == {"message": "Arguments are not correct"} -def test_import_assets_no_contents(mocker: MockFixture, client: Any) -> None: +def test_import_assets_no_contents( + mocker: MockFixture, + client: Any, + full_api_access: None, +) -> None: """ Test error message when the ZIP bundle has no contents. """ - # grant access - mocker.patch( - "flask_appbuilder.security.decorators.verify_jwt_in_request", return_value=True - ) - mocker.patch.object(security_manager, "has_access", return_value=True) - mocked_contents = { "README.txt": "Something is wrong", } From 41bbf62e586933172bde4b4080a4f21fe1ccd290 Mon Sep 17 00:00:00 2001 From: mohittt8 Date: Fri, 17 Jun 2022 05:13:17 +0530 Subject: [PATCH 45/71] fix(presto): use correct timespec for presto (#20333) --- superset/db_engine_specs/presto.py | 2 +- tests/unit_tests/db_engine_specs/test_presto.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/superset/db_engine_specs/presto.py b/superset/db_engine_specs/presto.py index 9a72b76081658..d0621e288ef41 100644 --- a/superset/db_engine_specs/presto.py +++ b/superset/db_engine_specs/presto.py @@ -761,7 +761,7 @@ def convert_dttm( utils.TemporalType.TIMESTAMP, utils.TemporalType.TIMESTAMP_WITH_TIME_ZONE, ): - return f"""TIMESTAMP '{dttm.isoformat(timespec="microseconds", sep=" ")}'""" + return f"""TIMESTAMP '{dttm.isoformat(timespec="milliseconds", sep=" ")}'""" return None @classmethod diff --git a/tests/unit_tests/db_engine_specs/test_presto.py b/tests/unit_tests/db_engine_specs/test_presto.py index 512d03096b0b9..228427c9caa76 100644 --- a/tests/unit_tests/db_engine_specs/test_presto.py +++ b/tests/unit_tests/db_engine_specs/test_presto.py @@ -30,17 +30,17 @@ ( "TIMESTAMP", datetime(2022, 1, 1, 1, 23, 45, 600000), - "TIMESTAMP '2022-01-01 01:23:45.600000'", + "TIMESTAMP '2022-01-01 01:23:45.600'", ), ( "TIMESTAMP WITH TIME ZONE", datetime(2022, 1, 1, 1, 23, 45, 600000), - "TIMESTAMP '2022-01-01 01:23:45.600000'", + "TIMESTAMP '2022-01-01 01:23:45.600'", ), ( "TIMESTAMP WITH TIME ZONE", datetime(2022, 1, 1, 1, 23, 45, 600000, tzinfo=pytz.UTC), - "TIMESTAMP '2022-01-01 01:23:45.600000+00:00'", + "TIMESTAMP '2022-01-01 01:23:45.600+00:00'", ), ], ) From b32288fddfc077d941452245a4e8002335746ba4 Mon Sep 17 00:00:00 2001 From: Stephen Liu <750188453@qq.com> Date: Fri, 17 Jun 2022 10:52:26 +0800 Subject: [PATCH 46/71] fix(bar-chart-v2): remove marker from bar chart V2 (#20409) --- .../Timeseries/Regular/Bar/controlPanel.tsx | 34 ------------------- 1 file changed, 34 deletions(-) diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx index 2080fceef65df..1992f4a45609e 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx @@ -45,8 +45,6 @@ import { const { contributionMode, logAxis, - markerEnabled, - markerSize, minorSplitLine, rowLimit, truncateYAxis, @@ -341,38 +339,6 @@ const config: ControlPanelConfig = { controlSetRows: [ ['color_scheme'], ...showValueSection, - [ - { - name: 'markerEnabled', - config: { - type: 'CheckboxControl', - label: t('Marker'), - renderTrigger: true, - default: markerEnabled, - description: t( - 'Draw a marker on data points. Only applicable for line types.', - ), - }, - }, - ], - [ - { - name: 'markerSize', - config: { - type: 'SliderControl', - label: t('Marker Size'), - renderTrigger: true, - min: 0, - max: 20, - default: markerSize, - description: t( - 'Size of marker. Also applies to forecast observations.', - ), - visibility: ({ controls }: ControlPanelsContainerProps) => - Boolean(controls?.markerEnabled?.value), - }, - }, - ], [ { name: 'zoomable', From fa7f144a687a438f7a67c99b167dd4af10471712 Mon Sep 17 00:00:00 2001 From: Stephen Liu <750188453@qq.com> Date: Fri, 17 Jun 2022 14:29:57 +0800 Subject: [PATCH 47/71] fix: rm eslint-plugin-translation-vars engine requirement (#20420) --- .../tools/eslint-plugin-translation-vars/package.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/superset-frontend/tools/eslint-plugin-translation-vars/package.json b/superset-frontend/tools/eslint-plugin-translation-vars/package.json index d4353a88df3d2..83e8c184d3b18 100644 --- a/superset-frontend/tools/eslint-plugin-translation-vars/package.json +++ b/superset-frontend/tools/eslint-plugin-translation-vars/package.json @@ -12,9 +12,5 @@ "dependencies": {}, "peerDependencies": { "eslint": ">=0.8.0" - }, - "engines": { - "node": "^16.9.1", - "npm": "^7.5.4" } } From f6f93aad37e6258ec27af1b39335d0de9163210d Mon Sep 17 00:00:00 2001 From: jiAng <464816158@qq.com> Date: Fri, 17 Jun 2022 19:54:27 +0800 Subject: [PATCH 48/71] fix(cosmetic): cannot find m-r-10 class in superset.less (#20276) * fix(cosmetic): cannot find m-r-10 class in superset.less * fix: remove .m-r-10 class and use emotion instead * Update superset-frontend/src/components/Datasource/CollectionTable.tsx Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com> * Update superset-frontend/src/components/Datasource/DatasourceEditor.jsx Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com> Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com> --- .../src/components/Datasource/CollectionTable.tsx | 11 +++++++++-- .../src/components/Datasource/DatasourceEditor.jsx | 11 +++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/superset-frontend/src/components/Datasource/CollectionTable.tsx b/superset-frontend/src/components/Datasource/CollectionTable.tsx index 7ce0bfb95fbdc..194d3765792c9 100644 --- a/superset-frontend/src/components/Datasource/CollectionTable.tsx +++ b/superset-frontend/src/components/Datasource/CollectionTable.tsx @@ -130,6 +130,13 @@ const CrudButtonWrapper = styled.div` ${({ theme }) => `margin-bottom: ${theme.gridUnit * 2}px`} `; +const StyledButtonWrapper = styled.span` + ${({ theme }) => ` + margin-top: ${theme.gridUnit * 3}px; + margin-left: ${theme.gridUnit * 3}px; + `} +`; + export default class CRUDCollection extends React.PureComponent< CRUDCollectionProps, CRUDCollectionState @@ -424,7 +431,7 @@ export default class CRUDCollection extends React.PureComponent< <> {this.props.allowAddItem && ( - + - + )} ` + margin-top: ${theme.gridUnit * 3}px; + margin-left: ${theme.gridUnit * 3}px; + `} +`; + const checkboxGenerator = (d, onChange) => ( ); @@ -1361,7 +1368,7 @@ class DatasourceEditor extends React.PureComponent { > - + - + Date: Fri, 17 Jun 2022 20:25:41 +0530 Subject: [PATCH 49/71] docs: Added details to Druid connection string (#20264) * Added details to Druid connection string While linking Superset to Druid the correct connection string is very important however in the docs it is not well explained , so I added a little description to it * Update druid.mdx --- docs/docs/databases/druid.mdx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/docs/databases/druid.mdx b/docs/docs/databases/druid.mdx index 6641898aa4973..95e9bbc15a8a9 100644 --- a/docs/docs/databases/druid.mdx +++ b/docs/docs/databases/druid.mdx @@ -18,6 +18,12 @@ The connection string looks like: ``` druid://:@:/druid/v2/sql ``` +Here's a breakdown of the key components of this connection string: + +User: username portion of the credentials needed to connect to your database +Password: password portion of the credentials needed to connect to your database +Host: IP address (or URL) of the host machine that's running your database +Port: specific port that's exposed on your host machine where your database is running ### Customizing Druid Connection From 9f74fb7a849fbf4219ed584118a6bdf149c53e68 Mon Sep 17 00:00:00 2001 From: Cody Leff Date: Fri, 17 Jun 2022 10:24:58 -0600 Subject: [PATCH 50/71] Skip flaky Cypress test. (#20417) --- .../cypress-base/cypress/integration/explore/control.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset-frontend/cypress-base/cypress/integration/explore/control.test.ts b/superset-frontend/cypress-base/cypress/integration/explore/control.test.ts index 6bc0840540bed..18bf8859a8ef9 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/control.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/explore/control.test.ts @@ -127,7 +127,7 @@ describe('Test datatable', () => { cy.get('[data-test="row-count-label"]').contains('26 rows'); cy.get('.ant-empty-description').should('not.exist'); }); - it('Datapane loads view samples', () => { + it.skip('Datapane loads view samples', () => { cy.contains('Samples').click(); cy.get('[data-test="row-count-label"]').contains('1k rows'); cy.get('.ant-empty-description').should('not.exist'); From f53018c7c5ebbec04ffd879e1b09fb4a3ffa5609 Mon Sep 17 00:00:00 2001 From: Lily Kuang Date: Fri, 17 Jun 2022 12:57:51 -0700 Subject: [PATCH 51/71] feat(embedded): enforce allow domains (#20251) * feat(embedded): enforce allow domains * check referrer in view * remove frontend check --- superset/embedded/view.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/superset/embedded/view.py b/superset/embedded/view.py index 487850b728851..b64bcf6fc28fb 100644 --- a/superset/embedded/view.py +++ b/superset/embedded/view.py @@ -17,9 +17,10 @@ import json from typing import Callable -from flask import abort +from flask import abort, request from flask_appbuilder import expose from flask_login import AnonymousUserMixin, LoginManager +from flask_wtf.csrf import same_origin from superset import event_logger, is_feature_enabled, security_manager from superset.embedded.dao import EmbeddedDAO @@ -50,9 +51,20 @@ def embedded( abort(404) embedded = EmbeddedDAO.find_by_id(uuid) + if not embedded: abort(404) + # validate request referrer in allowed domains + is_referrer_allowed = not embedded.allowed_domains + for domain in embedded.allowed_domains: + if same_origin(request.referrer, domain): + is_referrer_allowed = True + break + + if not is_referrer_allowed: + abort(403) + # Log in as an anonymous user, just for this view. # This view needs to be visible to all users, # and building the page fails if g.user and/or ctx.user aren't present. From ab9f72f1a1359a59e64afd9e820d5823fd53b77b Mon Sep 17 00:00:00 2001 From: Lily Kuang Date: Fri, 17 Jun 2022 20:01:08 -0700 Subject: [PATCH 52/71] fix(embedded): CSV download for chart (#20261) * move postForm to superset client * lint * fix lint * fix type * update tests * add tests * add test for form submit * add test for request form * lint * fix test * fix tests * more tests * more tests * test * lint * more test for postForm * lint * Update superset-frontend/packages/superset-ui-core/test/connection/SupersetClientClass.test.ts Co-authored-by: David Aaron Suddjian <1858430+suddjian@users.noreply.github.com> * update tests * remove useless test * make test cover happy * make test cover happy * make test cover happy * make codecov happy * make codecov happy Co-authored-by: David Aaron Suddjian <1858430+suddjian@users.noreply.github.com> --- .../src/connection/SupersetClient.ts | 1 + .../src/connection/SupersetClientClass.ts | 30 +++++ .../superset-ui-core/src/connection/types.ts | 1 + .../test/connection/SupersetClient.test.ts | 6 +- .../connection/SupersetClientClass.test.ts | 103 ++++++++++++++++++ .../src/components/Chart/chartAction.js | 6 +- .../DatasourceControl.test.tsx | 3 +- .../controls/DatasourceControl/index.jsx | 16 ++- .../exploreUtils/exploreUtils.test.jsx | 17 ++- .../src/explore/exploreUtils/index.js | 31 +----- superset/security/manager.py | 4 +- tests/integration_tests/security_tests.py | 12 ++ 12 files changed, 190 insertions(+), 40 deletions(-) diff --git a/superset-frontend/packages/superset-ui-core/src/connection/SupersetClient.ts b/superset-frontend/packages/superset-ui-core/src/connection/SupersetClient.ts index a5ab65d097cd2..530c710809068 100644 --- a/superset-frontend/packages/superset-ui-core/src/connection/SupersetClient.ts +++ b/superset-frontend/packages/superset-ui-core/src/connection/SupersetClient.ts @@ -45,6 +45,7 @@ const SupersetClient: SupersetClientInterface = { init: force => getInstance().init(force), isAuthenticated: () => getInstance().isAuthenticated(), post: request => getInstance().post(request), + postForm: (...args) => getInstance().postForm(...args), put: request => getInstance().put(request), reAuthenticate: () => getInstance().reAuthenticate(), request: request => getInstance().request(request), diff --git a/superset-frontend/packages/superset-ui-core/src/connection/SupersetClientClass.ts b/superset-frontend/packages/superset-ui-core/src/connection/SupersetClientClass.ts index 7a6dfd97b0207..b7281d025903c 100644 --- a/superset-frontend/packages/superset-ui-core/src/connection/SupersetClientClass.ts +++ b/superset-frontend/packages/superset-ui-core/src/connection/SupersetClientClass.ts @@ -119,6 +119,36 @@ export default class SupersetClientClass { return this.getCSRFToken(); } + async postForm(url: string, payload: Record, target = '_blank') { + if (url) { + await this.ensureAuth(); + const hiddenForm = document.createElement('form'); + hiddenForm.action = url; + hiddenForm.method = 'POST'; + hiddenForm.target = target; + const payloadWithToken: Record = { + ...payload, + csrf_token: this.csrfToken!, + }; + + if (this.guestToken) { + payloadWithToken.guest_token = this.guestToken; + } + + Object.entries(payloadWithToken).forEach(([key, value]) => { + const data = document.createElement('input'); + data.type = 'hidden'; + data.name = key; + data.value = value; + hiddenForm.appendChild(data); + }); + + document.body.appendChild(hiddenForm); + hiddenForm.submit(); + document.body.removeChild(hiddenForm); + } + } + async reAuthenticate() { return this.init(true); } diff --git a/superset-frontend/packages/superset-ui-core/src/connection/types.ts b/superset-frontend/packages/superset-ui-core/src/connection/types.ts index 0ab382917e170..06025956754dd 100644 --- a/superset-frontend/packages/superset-ui-core/src/connection/types.ts +++ b/superset-frontend/packages/superset-ui-core/src/connection/types.ts @@ -146,6 +146,7 @@ export interface SupersetClientInterface | 'delete' | 'get' | 'post' + | 'postForm' | 'put' | 'request' | 'init' diff --git a/superset-frontend/packages/superset-ui-core/test/connection/SupersetClient.test.ts b/superset-frontend/packages/superset-ui-core/test/connection/SupersetClient.test.ts index 227d59b17288a..17a07f3c727e6 100644 --- a/superset-frontend/packages/superset-ui-core/test/connection/SupersetClient.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/connection/SupersetClient.test.ts @@ -30,21 +30,23 @@ describe('SupersetClient', () => { afterEach(SupersetClient.reset); - it('exposes reset, configure, init, get, post, isAuthenticated, and reAuthenticate methods', () => { + it('exposes reset, configure, init, get, post, postForm, isAuthenticated, and reAuthenticate methods', () => { expect(typeof SupersetClient.configure).toBe('function'); expect(typeof SupersetClient.init).toBe('function'); expect(typeof SupersetClient.get).toBe('function'); expect(typeof SupersetClient.post).toBe('function'); + expect(typeof SupersetClient.postForm).toBe('function'); expect(typeof SupersetClient.isAuthenticated).toBe('function'); expect(typeof SupersetClient.reAuthenticate).toBe('function'); expect(typeof SupersetClient.request).toBe('function'); expect(typeof SupersetClient.reset).toBe('function'); }); - it('throws if you call init, get, post, isAuthenticated, or reAuthenticate before configure', () => { + it('throws if you call init, get, post, postForm, isAuthenticated, or reAuthenticate before configure', () => { expect(SupersetClient.init).toThrow(); expect(SupersetClient.get).toThrow(); expect(SupersetClient.post).toThrow(); + expect(SupersetClient.postForm).toThrow(); expect(SupersetClient.isAuthenticated).toThrow(); expect(SupersetClient.reAuthenticate).toThrow(); expect(SupersetClient.request).toThrow(); diff --git a/superset-frontend/packages/superset-ui-core/test/connection/SupersetClientClass.test.ts b/superset-frontend/packages/superset-ui-core/test/connection/SupersetClientClass.test.ts index ef31e5d35d857..4db26b05b4151 100644 --- a/superset-frontend/packages/superset-ui-core/test/connection/SupersetClientClass.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/connection/SupersetClientClass.test.ts @@ -605,4 +605,107 @@ describe('SupersetClientClass', () => { } }); }); + + describe('.postForm()', () => { + const protocol = 'https:'; + const host = 'host'; + const mockPostFormEndpoint = '/post_form/url'; + const mockPostFormUrl = `${protocol}//${host}${mockPostFormEndpoint}`; + const guestToken = 'test-guest-token'; + const postFormPayload = { number: 123, array: [1, 2, 3] }; + + let authSpy: jest.SpyInstance; + let client: SupersetClientClass; + let appendChild: any; + let removeChild: any; + let submit: any; + let createElement: any; + + beforeEach(async () => { + client = new SupersetClientClass({ protocol, host }); + authSpy = jest.spyOn(SupersetClientClass.prototype, 'ensureAuth'); + await client.init(); + appendChild = jest.fn(); + removeChild = jest.fn(); + submit = jest.fn(); + createElement = jest.fn(() => ({ + appendChild: jest.fn(), + submit, + })); + + document.createElement = createElement as any; + document.body.appendChild = appendChild; + document.body.removeChild = removeChild; + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('makes postForm request', async () => { + await client.postForm(mockPostFormUrl, {}); + + const hiddenForm = createElement.mock.results[0].value; + const csrfTokenInput = createElement.mock.results[1].value; + + expect(createElement.mock.calls).toHaveLength(2); + + expect(hiddenForm.action).toBe(mockPostFormUrl); + expect(hiddenForm.method).toBe('POST'); + expect(hiddenForm.target).toBe('_blank'); + + expect(csrfTokenInput.type).toBe('hidden'); + expect(csrfTokenInput.name).toBe('csrf_token'); + expect(csrfTokenInput.value).toBe(1234); + + expect(appendChild.mock.calls).toHaveLength(1); + expect(removeChild.mock.calls).toHaveLength(1); + expect(authSpy).toHaveBeenCalledTimes(1); + }); + + it('makes postForm request with guest token', async () => { + client = new SupersetClientClass({ protocol, host, guestToken }); + await client.init(); + + await client.postForm(mockPostFormUrl, {}); + + const guestTokenInput = createElement.mock.results[2].value; + + expect(createElement.mock.calls).toHaveLength(3); + + expect(guestTokenInput.type).toBe('hidden'); + expect(guestTokenInput.name).toBe('guest_token'); + expect(guestTokenInput.value).toBe(guestToken); + + expect(appendChild.mock.calls).toHaveLength(1); + expect(removeChild.mock.calls).toHaveLength(1); + expect(authSpy).toHaveBeenCalledTimes(1); + }); + + it('makes postForm request with payload', async () => { + await client.postForm(mockPostFormUrl, { form_data: postFormPayload }); + + const postFormPayloadInput = createElement.mock.results[1].value; + + expect(createElement.mock.calls).toHaveLength(3); + + expect(postFormPayloadInput.type).toBe('hidden'); + expect(postFormPayloadInput.name).toBe('form_data'); + expect(postFormPayloadInput.value).toBe(postFormPayload); + + expect(appendChild.mock.calls).toHaveLength(1); + expect(removeChild.mock.calls).toHaveLength(1); + expect(submit.mock.calls).toHaveLength(1); + expect(authSpy).toHaveBeenCalledTimes(1); + }); + + it('should do nothing when url is empty string', async () => { + const result = await client.postForm('', {}); + expect(result).toBeUndefined(); + expect(createElement.mock.calls).toHaveLength(0); + expect(appendChild.mock.calls).toHaveLength(0); + expect(removeChild.mock.calls).toHaveLength(0); + expect(authSpy).toHaveBeenCalledTimes(0); + }); + }); }); diff --git a/superset-frontend/src/components/Chart/chartAction.js b/superset-frontend/src/components/Chart/chartAction.js index d52ac79177da0..b9aeffec4a4b8 100644 --- a/superset-frontend/src/components/Chart/chartAction.js +++ b/superset-frontend/src/components/Chart/chartAction.js @@ -27,7 +27,6 @@ import { getExploreUrl, getLegacyEndpointType, buildV1ChartDataPayload, - postForm, shouldUseLegacyApi, getChartDataUri, } from 'src/explore/exploreUtils'; @@ -40,6 +39,7 @@ import { addDangerToast } from 'src/components/MessageToasts/actions'; import { logEvent } from 'src/logger/actions'; import { Logger, LOG_ACTIONS_LOAD_CHART } from 'src/logger/LogUtils'; import { getClientErrorObject } from 'src/utils/getClientErrorObject'; +import { safeStringify } from 'src/utils/safeStringify'; import { allowCrossDomain as domainShardingEnabled } from 'src/utils/hostNamesConfig'; import { updateDataMask } from 'src/dataMask/actions'; import { waitForAsyncData } from 'src/middleware/asyncEvent'; @@ -563,7 +563,9 @@ export function redirectSQLLab(formData) { datasourceKey: formData.datasource, sql: json.result[0].query, }; - postForm(redirectUrl, payload); + SupersetClient.postForm(redirectUrl, { + form_data: safeStringify(payload), + }); }) .catch(() => dispatch(addDangerToast(t('An error occurred while loading the SQL'))), diff --git a/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.tsx b/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.tsx index dd43ed1a04049..8a65a1139df9c 100644 --- a/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.tsx +++ b/superset-frontend/src/explore/components/controls/DatasourceControl/DatasourceControl.test.tsx @@ -21,7 +21,6 @@ import React from 'react'; import { render, screen, act } from 'spec/helpers/testing-library'; import userEvent from '@testing-library/user-event'; import { SupersetClient, DatasourceType } from '@superset-ui/core'; -import * as Utils from 'src/explore/exploreUtils'; import DatasourceControl from '.'; const SupersetClientGet = jest.spyOn(SupersetClient, 'get'); @@ -142,7 +141,7 @@ test('Click on Edit dataset', async () => { test('Click on View in SQL Lab', async () => { const props = createProps(); - const postFormSpy = jest.spyOn(Utils, 'postForm'); + const postFormSpy = jest.spyOn(SupersetClient, 'postForm'); postFormSpy.mockImplementation(jest.fn()); render(, { diff --git a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx b/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx index 15d07fd6dd9dd..615c61c2bd20a 100644 --- a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx +++ b/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx @@ -19,7 +19,13 @@ */ import React from 'react'; import PropTypes from 'prop-types'; -import { t, styled, withTheme, DatasourceType } from '@superset-ui/core'; +import { + DatasourceType, + SupersetClient, + styled, + t, + withTheme, +} from '@superset-ui/core'; import { getUrlParam } from 'src/utils/urlUtils'; import { AntdDropdown } from 'src/components'; @@ -30,13 +36,13 @@ import { ChangeDatasourceModal, DatasourceModal, } from 'src/components/Datasource'; -import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal'; -import { postForm } from 'src/explore/exploreUtils'; import Button from 'src/components/Button'; import ErrorAlert from 'src/components/ErrorMessage/ErrorAlert'; import WarningIconWithTooltip from 'src/components/WarningIconWithTooltip'; import { URL_PARAMS } from 'src/constants'; import { isUserAdmin } from 'src/dashboard/util/findPermission'; +import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal'; +import { safeStringify } from 'src/utils/safeStringify'; const propTypes = { actions: PropTypes.object.isRequired, @@ -193,7 +199,9 @@ class DatasourceControl extends React.PureComponent { datasourceKey: `${datasource.id}__${datasource.type}`, sql: datasource.sql, }; - postForm('/superset/sqllab/', payload); + SupersetClient.postForm('/superset/sqllab/', { + form_data: safeStringify(payload), + }); } break; diff --git a/superset-frontend/src/explore/exploreUtils/exploreUtils.test.jsx b/superset-frontend/src/explore/exploreUtils/exploreUtils.test.jsx index f52b575e2d282..b385099699cb4 100644 --- a/superset-frontend/src/explore/exploreUtils/exploreUtils.test.jsx +++ b/superset-frontend/src/explore/exploreUtils/exploreUtils.test.jsx @@ -21,13 +21,14 @@ import sinon from 'sinon'; import URI from 'urijs'; import { buildV1ChartDataPayload, + exploreChart, getExploreUrl, - shouldUseLegacyApi, getSimpleSQLExpression, + shouldUseLegacyApi, } from 'src/explore/exploreUtils'; import { DashboardStandaloneMode } from 'src/dashboard/util/constants'; import * as hostNamesConfig from 'src/utils/hostNamesConfig'; -import { getChartMetadataRegistry } from '@superset-ui/core'; +import { getChartMetadataRegistry, SupersetClient } from '@superset-ui/core'; describe('exploreUtils', () => { const { location } = window; @@ -275,4 +276,16 @@ describe('exploreUtils', () => { ); }); }); + + describe('.exploreChart()', () => { + it('postForm', () => { + const postFormSpy = jest.spyOn(SupersetClient, 'postForm'); + postFormSpy.mockImplementation(jest.fn()); + + exploreChart({ + formData: { ...formData, viz_type: 'my_custom_viz' }, + }); + expect(postFormSpy).toBeCalledTimes(1); + }); + }); }); diff --git a/superset-frontend/src/explore/exploreUtils/index.js b/superset-frontend/src/explore/exploreUtils/index.js index 127766e7f314f..0e8be9feaf83d 100644 --- a/superset-frontend/src/explore/exploreUtils/index.js +++ b/superset-frontend/src/explore/exploreUtils/index.js @@ -25,6 +25,7 @@ import { ensureIsArray, getChartBuildQueryRegistry, getChartMetadataRegistry, + SupersetClient, } from '@superset-ui/core'; import { availableDomains } from 'src/utils/hostNamesConfig'; import { safeStringify } from 'src/utils/safeStringify'; @@ -234,31 +235,6 @@ export const buildV1ChartDataPayload = ({ export const getLegacyEndpointType = ({ resultType, resultFormat }) => resultFormat === 'csv' ? resultFormat : resultType; -export function postForm(url, payload, target = '_blank') { - if (!url) { - return; - } - - const hiddenForm = document.createElement('form'); - hiddenForm.action = url; - hiddenForm.method = 'POST'; - hiddenForm.target = target; - const token = document.createElement('input'); - token.type = 'hidden'; - token.name = 'csrf_token'; - token.value = (document.getElementById('csrf_token') || {}).value; - hiddenForm.appendChild(token); - const data = document.createElement('input'); - data.type = 'hidden'; - data.name = 'form_data'; - data.value = safeStringify(payload); - hiddenForm.appendChild(data); - - document.body.appendChild(hiddenForm); - hiddenForm.submit(); - document.body.removeChild(hiddenForm); -} - export const exportChart = ({ formData, resultFormat = 'json', @@ -286,7 +262,8 @@ export const exportChart = ({ ownState, }); } - postForm(url, payload); + + SupersetClient.postForm(url, { form_data: safeStringify(payload) }); }; export const exploreChart = formData => { @@ -295,7 +272,7 @@ export const exploreChart = formData => { endpointType: 'base', allowDomainSharding: false, }); - postForm(url, formData); + SupersetClient.postForm(url, { form_data: safeStringify(formData) }); }; export const useDebouncedEffect = (effect, delay, deps) => { diff --git a/superset/security/manager.py b/superset/security/manager.py index b231b93b48655..6157959aa3739 100644 --- a/superset/security/manager.py +++ b/superset/security/manager.py @@ -1383,7 +1383,9 @@ def get_guest_user_from_request(self, req: Request) -> Optional[GuestUser]: :return: A guest user object """ - raw_token = req.headers.get(current_app.config["GUEST_TOKEN_HEADER_NAME"]) + raw_token = req.headers.get( + current_app.config["GUEST_TOKEN_HEADER_NAME"] + ) or req.form.get("guest_token") if raw_token is None: return None diff --git a/tests/integration_tests/security_tests.py b/tests/integration_tests/security_tests.py index e66bf02e82cb3..a70146db68321 100644 --- a/tests/integration_tests/security_tests.py +++ b/tests/integration_tests/security_tests.py @@ -1064,6 +1064,7 @@ def test_get_user_datasources_gamma_with_schema( class FakeRequest: headers: Any = {} + form: Any = {} class TestGuestTokens(SupersetTestCase): @@ -1111,6 +1112,17 @@ def test_get_guest_user(self): self.assertIsNotNone(guest_user) self.assertEqual("test_guest", guest_user.username) + def test_get_guest_user_with_request_form(self): + token = self.create_guest_token() + fake_request = FakeRequest() + fake_request.headers[current_app.config["GUEST_TOKEN_HEADER_NAME"]] = None + fake_request.form["guest_token"] = token + + guest_user = security_manager.get_guest_user_from_request(fake_request) + + self.assertIsNotNone(guest_user) + self.assertEqual("test_guest", guest_user.username) + @patch("superset.security.SupersetSecurityManager._get_current_epoch_time") def test_get_guest_user_expired_token(self, get_time_mock): # make a just-expired token From c2f01a676c5e5eb53b98a6a609674c8342f8a0ac Mon Sep 17 00:00:00 2001 From: Diego Medina Date: Sun, 19 Jun 2022 11:17:06 -0300 Subject: [PATCH 53/71] fix(dashboard): new created chart did not have high lighted effect when using the permalink of chart share in dashboard (#20411) --- superset-frontend/src/dashboard/actions/hydrate.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/superset-frontend/src/dashboard/actions/hydrate.js b/superset-frontend/src/dashboard/actions/hydrate.js index 4174d9780ba64..ccc8288e6158e 100644 --- a/superset-frontend/src/dashboard/actions/hydrate.js +++ b/superset-frontend/src/dashboard/actions/hydrate.js @@ -127,6 +127,8 @@ export const hydrateDashboard = const dashboardFilters = {}; const slices = {}; const sliceIds = new Set(); + const slicesFromExploreCount = new Map(); + chartData.forEach(slice => { const key = slice.slice_id; const form_data = { @@ -182,6 +184,10 @@ export const hydrateDashboard = (newSlicesContainer.parents || []).slice(), ); + const count = (slicesFromExploreCount.get(slice.slice_id) ?? 0) + 1; + chartHolder.id = `${CHART_TYPE}-explore-${slice.slice_id}-${count}`; + slicesFromExploreCount.set(slice.slice_id, count); + layout[chartHolder.id] = chartHolder; newSlicesContainer.children.push(chartHolder.id); chartIdToLayoutId[chartHolder.meta.chartId] = chartHolder.id; From 8b7262fa9040b6bc956dfa2c191953fe3b65bea6 Mon Sep 17 00:00:00 2001 From: Simon Thelin Date: Mon, 20 Jun 2022 00:28:59 +0100 Subject: [PATCH 54/71] fix(20428): Address-Presto/Trino-Poll-Issue-Refactor (#20434) * fix(20428)-Address-Presto/Trino-Poll-Issue-Refacto r Update linter * Update to only use BaseEngineSpec handle_cursor * Fix CI Co-authored-by: John Bodley <4567245+john-bodley@users.noreply.github.com> --- superset/db_engine_specs/presto.py | 4 ---- superset/db_engine_specs/trino.py | 36 +++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/superset/db_engine_specs/presto.py b/superset/db_engine_specs/presto.py index d0621e288ef41..cd6fa032b39ed 100644 --- a/superset/db_engine_specs/presto.py +++ b/superset/db_engine_specs/presto.py @@ -949,11 +949,7 @@ def get_create_view( sql = f"SHOW CREATE VIEW {schema}.{table}" try: cls.execute(cursor, sql) - polled = cursor.poll() - while polled: - time.sleep(0.2) - polled = cursor.poll() except DatabaseError: # not a VIEW return None rows = cls.fetch_data(cursor, 1) diff --git a/superset/db_engine_specs/trino.py b/superset/db_engine_specs/trino.py index 46e3ed55dec0c..acddb97100266 100644 --- a/superset/db_engine_specs/trino.py +++ b/superset/db_engine_specs/trino.py @@ -15,15 +15,18 @@ # specific language governing permissions and limitations # under the License. import logging -from typing import Any, Dict, Optional, TYPE_CHECKING +from typing import Any, Dict, List, Optional, TYPE_CHECKING import simplejson as json from flask import current_app +from sqlalchemy.engine.reflection import Inspector from sqlalchemy.engine.url import URL +from sqlalchemy.orm import Session from superset.databases.utils import make_url_safe from superset.db_engine_specs.base import BaseEngineSpec from superset.db_engine_specs.presto import PrestoEngineSpec +from superset.models.sql_lab import Query from superset.utils import core as utils if TYPE_CHECKING: @@ -77,6 +80,37 @@ def modify_url_for_impersonation( def get_allow_cost_estimate(cls, extra: Dict[str, Any]) -> bool: return True + @classmethod + def get_table_names( + cls, + database: "Database", + inspector: Inspector, + schema: Optional[str], + ) -> List[str]: + return BaseEngineSpec.get_table_names( + database=database, + inspector=inspector, + schema=schema, + ) + + @classmethod + def get_view_names( + cls, + database: "Database", + inspector: Inspector, + schema: Optional[str], + ) -> List[str]: + return BaseEngineSpec.get_view_names( + database=database, + inspector=inspector, + schema=schema, + ) + + @classmethod + def handle_cursor(cls, cursor: Any, query: Query, session: Session) -> None: + """Updates progress information""" + BaseEngineSpec.handle_cursor(cursor=cursor, query=query, session=session) + @staticmethod def get_extra_params(database: "Database") -> Dict[str, Any]: """ From 8b0bee5e8bc724e3942e71f073694d85acadc561 Mon Sep 17 00:00:00 2001 From: John Bodley <4567245+john-bodley@users.noreply.github.com> Date: Sun, 19 Jun 2022 21:53:09 -0700 Subject: [PATCH 55/71] [fbprophet] Fix frequencies (#20326) --- superset/utils/pandas_postprocessing/utils.py | 8 +-- .../pandas_postprocessing/test_prophet.py | 61 +++++++++++++++++++ 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/superset/utils/pandas_postprocessing/utils.py b/superset/utils/pandas_postprocessing/utils.py index 46f5a0c50529c..ab39135918b7c 100644 --- a/superset/utils/pandas_postprocessing/utils.py +++ b/superset/utils/pandas_postprocessing/utils.py @@ -86,10 +86,10 @@ "P1M": "M", "P3M": "Q", "P1Y": "A", - "1969-12-28T00:00:00Z/P1W": "W", - "1969-12-29T00:00:00Z/P1W": "W", - "P1W/1970-01-03T00:00:00Z": "W", - "P1W/1970-01-04T00:00:00Z": "W", + "1969-12-28T00:00:00Z/P1W": "W-SUN", + "1969-12-29T00:00:00Z/P1W": "W-MON", + "P1W/1970-01-03T00:00:00Z": "W-SAT", + "P1W/1970-01-04T00:00:00Z": "W-SUN", } RESAMPLE_METHOD = ("asfreq", "bfill", "ffill", "linear", "median", "mean", "sum") diff --git a/tests/unit_tests/pandas_postprocessing/test_prophet.py b/tests/unit_tests/pandas_postprocessing/test_prophet.py index e4f3ed8cfc36d..6da3a7a591a3d 100644 --- a/tests/unit_tests/pandas_postprocessing/test_prophet.py +++ b/tests/unit_tests/pandas_postprocessing/test_prophet.py @@ -17,6 +17,7 @@ from datetime import datetime from importlib.util import find_spec +import pandas as pd import pytest from superset.exceptions import InvalidPostProcessingError @@ -50,6 +51,66 @@ def test_prophet_valid(): assert df[DTTM_ALIAS].iloc[-1].to_pydatetime() == datetime(2022, 5, 31) assert len(df) == 9 + df = prophet( + df=pd.DataFrame( + { + "__timestamp": [datetime(2022, 1, 2), datetime(2022, 1, 9)], + "x": [1, 1], + } + ), + time_grain="P1W", + periods=1, + confidence_interval=0.9, + ) + + assert df[DTTM_ALIAS].iloc[-1].to_pydatetime() == datetime(2022, 1, 16) + assert len(df) == 3 + + df = prophet( + df=pd.DataFrame( + { + "__timestamp": [datetime(2022, 1, 2), datetime(2022, 1, 9)], + "x": [1, 1], + } + ), + time_grain="1969-12-28T00:00:00Z/P1W", + periods=1, + confidence_interval=0.9, + ) + + assert df[DTTM_ALIAS].iloc[-1].to_pydatetime() == datetime(2022, 1, 16) + assert len(df) == 3 + + df = prophet( + df=pd.DataFrame( + { + "__timestamp": [datetime(2022, 1, 3), datetime(2022, 1, 10)], + "x": [1, 1], + } + ), + time_grain="1969-12-29T00:00:00Z/P1W", + periods=1, + confidence_interval=0.9, + ) + + assert df[DTTM_ALIAS].iloc[-1].to_pydatetime() == datetime(2022, 1, 17) + assert len(df) == 3 + + df = prophet( + df=pd.DataFrame( + { + "__timestamp": [datetime(2022, 1, 8), datetime(2022, 1, 15)], + "x": [1, 1], + } + ), + time_grain="P1W/1970-01-03T00:00:00Z", + periods=1, + confidence_interval=0.9, + ) + + assert df[DTTM_ALIAS].iloc[-1].to_pydatetime() == datetime(2022, 1, 22) + assert len(df) == 3 + def test_prophet_valid_zero_periods(): pytest.importorskip("prophet") From 60eb1094a4f270ba8931f3c2e1656bd257a948fb Mon Sep 17 00:00:00 2001 From: Daniel Vaz Gaspar Date: Mon, 20 Jun 2022 13:52:05 +0100 Subject: [PATCH 56/71] feat: add name, description and non null tables to RLS (#20432) * feat: add name, description and non null tables to RLS * add validation * add and fix tests * fix sqlite migration * improve default value for name --- superset/connectors/sqla/models.py | 3 +- superset/connectors/sqla/views.py | 45 ++++++++- ...7_f3afaf1f11f0_add_unique_name_desc_rls.py | 79 ++++++++++++++++ .../security/row_level_security_tests.py | 93 ++++++++++++++++++- tests/unit_tests/sql_lab_test.py | 1 + 5 files changed, 214 insertions(+), 7 deletions(-) create mode 100644 superset/migrations/versions/2022-06-19_16-17_f3afaf1f11f0_add_unique_name_desc_rls.py diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py index 3b404743317a0..ff90cb2a56fef 100644 --- a/superset/connectors/sqla/models.py +++ b/superset/connectors/sqla/models.py @@ -2482,6 +2482,8 @@ class RowLevelSecurityFilter(Model, AuditMixinNullable): __tablename__ = "row_level_security_filters" id = Column(Integer, primary_key=True) + name = Column(String(255), unique=True, nullable=False) + description = Column(Text) filter_type = Column( Enum(*[filter_type.value for filter_type in utils.RowLevelSecurityFilterType]) ) @@ -2494,5 +2496,4 @@ class RowLevelSecurityFilter(Model, AuditMixinNullable): tables = relationship( SqlaTable, secondary=RLSFilterTables, backref="row_level_security_filters" ) - clause = Column(Text, nullable=False) diff --git a/superset/connectors/sqla/views.py b/superset/connectors/sqla/views.py index ef5afa5d05b28..e3a3725f311b4 100644 --- a/superset/connectors/sqla/views.py +++ b/superset/connectors/sqla/views.py @@ -26,7 +26,7 @@ from flask_appbuilder.security.decorators import has_access from flask_babel import lazy_gettext as _ from wtforms.ext.sqlalchemy.fields import QuerySelectField -from wtforms.validators import Regexp +from wtforms.validators import DataRequired, Regexp from superset import app, db from superset.connectors.base.views import DatasourceModelView @@ -47,6 +47,19 @@ logger = logging.getLogger(__name__) +class SelectDataRequired(DataRequired): # pylint: disable=too-few-public-methods + """ + Select required flag on the input field will not work well on Chrome + Console error: + An invalid form control with name='tables' is not focusable. + + This makes a simple override to the DataRequired to be used specifically with + select fields + """ + + field_flags = () + + class TableColumnInlineView(CompactCRUDMixin, SupersetModelView): datamodel = SQLAInterface(models.TableColumn) # TODO TODO, review need for this on related_views @@ -272,21 +285,39 @@ class RowLevelSecurityFiltersModelView(SupersetModelView, DeleteMixin): edit_title = _("Edit Row level security filter") list_columns = [ + "name", "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"] + order_columns = ["name", "filter_type", "clause", "modified"] + edit_columns = [ + "name", + "description", + "filter_type", + "tables", + "roles", + "group_key", + "clause", + ] show_columns = edit_columns - search_columns = ("filter_type", "tables", "roles", "group_key", "clause") + search_columns = ( + "name", + "description", + "filter_type", + "tables", + "roles", + "group_key", + "clause", + ) add_columns = edit_columns base_order = ("changed_on", "desc") description_columns = { + "name": _("Choose a unique name"), + "description": _("Optionally add a detailed description"), "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 " @@ -319,12 +350,16 @@ class RowLevelSecurityFiltersModelView(SupersetModelView, DeleteMixin): ), } label_columns = { + "name": _("Name"), + "description": _("Description"), "tables": _("Tables"), "roles": _("Roles"), "clause": _("Clause"), "creator": _("Creator"), "modified": _("Modified"), } + validators_columns = {"tables": [SelectDataRequired()]} + if app.config["RLS_FORM_QUERY_REL_FIELDS"]: add_form_query_rel_fields = app.config["RLS_FORM_QUERY_REL_FIELDS"] edit_form_query_rel_fields = add_form_query_rel_fields diff --git a/superset/migrations/versions/2022-06-19_16-17_f3afaf1f11f0_add_unique_name_desc_rls.py b/superset/migrations/versions/2022-06-19_16-17_f3afaf1f11f0_add_unique_name_desc_rls.py new file mode 100644 index 0000000000000..0d8b3334a4fcf --- /dev/null +++ b/superset/migrations/versions/2022-06-19_16-17_f3afaf1f11f0_add_unique_name_desc_rls.py @@ -0,0 +1,79 @@ +# 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_unique_name_desc_rls + +Revision ID: f3afaf1f11f0 +Revises: e786798587de +Create Date: 2022-06-19 16:17:23.318618 + +""" + +# revision identifiers, used by Alembic. +revision = "f3afaf1f11f0" +down_revision = "e786798587de" + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import Session + +Base = declarative_base() + + +class RowLevelSecurityFilter(Base): + __tablename__ = "row_level_security_filters" + id = sa.Column(sa.Integer, primary_key=True) + name = sa.Column(sa.String(255), unique=True, nullable=False) + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + bind = op.get_bind() + session = Session(bind=bind) + + op.add_column( + "row_level_security_filters", sa.Column("name", sa.String(length=255)) + ) + op.add_column( + "row_level_security_filters", sa.Column("description", sa.Text(), nullable=True) + ) + + # Set initial default names make sure we can have unique non null values + all_rls = session.query(RowLevelSecurityFilter).all() + for rls in all_rls: + rls.name = f"rls-{rls.id}" + session.commit() + + # Now it's safe so set non-null and unique + # add unique constraint + with op.batch_alter_table("row_level_security_filters") as batch_op: + # batch mode is required for sqlite + batch_op.alter_column( + "name", + existing_type=sa.String(255), + nullable=False, + ) + batch_op.create_unique_constraint("uq_rls_name", ["name"]) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint("uq_rls_name", "row_level_security_filters", type_="unique") + op.drop_column("row_level_security_filters", "description") + op.drop_column("row_level_security_filters", "name") + # ### end Alembic commands ### diff --git a/tests/integration_tests/security/row_level_security_tests.py b/tests/integration_tests/security/row_level_security_tests.py index 1e46bfb996c5b..ebd95cae39bd7 100644 --- a/tests/integration_tests/security/row_level_security_tests.py +++ b/tests/integration_tests/security/row_level_security_tests.py @@ -25,7 +25,6 @@ from superset import db, security_manager from superset.connectors.sqla.models import RowLevelSecurityFilter, SqlaTable from superset.security.guest_token import ( - GuestTokenRlsRule, GuestTokenResourceType, GuestUser, ) @@ -82,6 +81,7 @@ def setUp(self): # Create regular RowLevelSecurityFilter (energy_usage, unicode_test) self.rls_entry1 = RowLevelSecurityFilter() + self.rls_entry1.name = "rls_entry1" self.rls_entry1.tables.extend( session.query(SqlaTable) .filter(SqlaTable.table_name.in_(["energy_usage", "unicode_test"])) @@ -96,6 +96,7 @@ def setUp(self): # Create regular RowLevelSecurityFilter (birth_names name starts with A or B) self.rls_entry2 = RowLevelSecurityFilter() + self.rls_entry2.name = "rls_entry2" self.rls_entry2.tables.extend( session.query(SqlaTable) .filter(SqlaTable.table_name.in_(["birth_names"])) @@ -109,6 +110,7 @@ def setUp(self): # Create Regular RowLevelSecurityFilter (birth_names name starts with Q) self.rls_entry3 = RowLevelSecurityFilter() + self.rls_entry3.name = "rls_entry3" self.rls_entry3.tables.extend( session.query(SqlaTable) .filter(SqlaTable.table_name.in_(["birth_names"])) @@ -122,6 +124,7 @@ def setUp(self): # Create Base RowLevelSecurityFilter (birth_names boys) self.rls_entry4 = RowLevelSecurityFilter() + self.rls_entry4.name = "rls_entry4" self.rls_entry4.tables.extend( session.query(SqlaTable) .filter(SqlaTable.table_name.in_(["birth_names"])) @@ -146,6 +149,94 @@ def tearDown(self): session.delete(self.get_user("NoRlsRoleUser")) session.commit() + @pytest.fixture() + def create_dataset(self): + with self.create_app().app_context(): + + dataset = SqlaTable(database_id=1, schema=None, table_name="table1") + db.session.add(dataset) + db.session.flush() + db.session.commit() + + yield dataset + + # rollback changes (assuming cascade delete) + db.session.delete(dataset) + db.session.commit() + + def _get_test_dataset(self): + return ( + db.session.query(SqlaTable).filter(SqlaTable.table_name == "table1") + ).one_or_none() + + @pytest.mark.usefixtures("create_dataset") + def test_model_view_rls_add_success(self): + self.login(username="admin") + test_dataset = self._get_test_dataset() + rv = self.client.post( + "/rowlevelsecurityfiltersmodelview/add", + data=dict( + name="rls1", + description="Some description", + filter_type="Regular", + tables=[test_dataset.id], + roles=[security_manager.find_role("Alpha").id], + group_key="group_key_1", + clause="client_id=1", + ), + follow_redirects=True, + ) + self.assertEqual(rv.status_code, 200) + rls1 = ( + db.session.query(RowLevelSecurityFilter).filter_by(name="rls1") + ).one_or_none() + assert rls1 is not None + + # Revert data changes + db.session.delete(rls1) + db.session.commit() + + @pytest.mark.usefixtures("create_dataset") + def test_model_view_rls_add_name_unique(self): + self.login(username="admin") + test_dataset = self._get_test_dataset() + rv = self.client.post( + "/rowlevelsecurityfiltersmodelview/add", + data=dict( + name="rls_entry1", + description="Some description", + filter_type="Regular", + tables=[test_dataset.id], + roles=[security_manager.find_role("Alpha").id], + group_key="group_key_1", + clause="client_id=1", + ), + follow_redirects=True, + ) + self.assertEqual(rv.status_code, 200) + data = rv.data.decode("utf-8") + assert "Already exists." in data + + @pytest.mark.usefixtures("create_dataset") + def test_model_view_rls_add_tables_required(self): + self.login(username="admin") + rv = self.client.post( + "/rowlevelsecurityfiltersmodelview/add", + data=dict( + name="rls1", + description="Some description", + filter_type="Regular", + tables=[], + roles=[security_manager.find_role("Alpha").id], + group_key="group_key_1", + clause="client_id=1", + ), + follow_redirects=True, + ) + self.assertEqual(rv.status_code, 200) + data = rv.data.decode("utf-8") + assert "This field is required." in data + @pytest.mark.usefixtures("load_energy_table_with_slice") def test_rls_filter_alters_energy_query(self): g.user = self.get_user(username="alpha") diff --git a/tests/unit_tests/sql_lab_test.py b/tests/unit_tests/sql_lab_test.py index 9950fb9fedda5..c5bfa4a16d600 100644 --- a/tests/unit_tests/sql_lab_test.py +++ b/tests/unit_tests/sql_lab_test.py @@ -186,6 +186,7 @@ def test_sql_lab_insert_rls( # now with RLS rls = RowLevelSecurityFilter( + name="sqllab_rls1", filter_type=RowLevelSecurityFilterType.REGULAR, tables=[SqlaTable(database_id=1, schema=None, table_name="t")], roles=[admin.roles[0]], From 111affdb024398cf9490369bf9d668ec54ea5ca2 Mon Sep 17 00:00:00 2001 From: Yongjie Zhao Date: Mon, 20 Jun 2022 22:36:27 +0800 Subject: [PATCH 57/71] chore: move xaxis to superset-ui (#20438) --- .../src/shared-controls/index.tsx | 27 ++++++++ .../plugin-chart-echarts/src/BoxPlot/types.ts | 3 +- .../src/Funnel/transformProps.ts | 3 +- .../plugin-chart-echarts/src/Funnel/types.ts | 8 +-- .../plugin-chart-echarts/src/Gauge/types.ts | 3 +- .../plugin-chart-echarts/src/Graph/types.ts | 8 +-- .../src/MixedTimeseries/controlPanel.tsx | 4 +- .../src/MixedTimeseries/types.ts | 10 +-- .../src/Pie/transformProps.ts | 3 +- .../plugin-chart-echarts/src/Pie/types.ts | 8 +-- .../src/Radar/transformProps.ts | 3 +- .../plugin-chart-echarts/src/Radar/types.ts | 2 +- .../src/Timeseries/Area/controlPanel.tsx | 6 +- .../Timeseries/Regular/Bar/controlPanel.tsx | 6 +- .../src/Timeseries/Regular/Bar/index.ts | 8 +-- .../{ => Regular/Line}/controlPanel.tsx | 10 +-- .../src/Timeseries/Regular/Line/index.ts | 8 +-- .../Regular/Scatter/controlPanel.tsx | 6 +- .../src/Timeseries/Regular/Scatter/index.ts | 8 +-- .../Regular/{ => SmoothLine}/controlPanel.tsx | 9 +-- .../Timeseries/Regular/SmoothLine/index.ts | 8 +-- .../src/Timeseries/Step/controlPanel.tsx | 8 +-- .../src/Timeseries/Step/index.ts | 8 +-- .../src/Timeseries/constants.ts | 66 +++++++++++++++++++ .../src/Timeseries/index.ts | 2 +- .../src/Timeseries/transformProps.ts | 2 +- .../src/Timeseries/types.ts | 41 ------------ .../plugin-chart-echarts/src/constants.ts | 25 ++++++- .../plugin-chart-echarts/src/controls.tsx | 42 ++---------- .../plugins/plugin-chart-echarts/src/index.ts | 2 +- .../plugins/plugin-chart-echarts/src/types.ts | 15 ----- .../test/BoxPlot/buildQuery.test.ts | 2 +- 32 files changed, 191 insertions(+), 173 deletions(-) rename superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/{ => Regular/Line}/controlPanel.tsx (97%) rename superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/{ => SmoothLine}/controlPanel.tsx (96%) create mode 100644 superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/constants.ts diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx index c5bc9d56d4805..d42d4155555d8 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx @@ -67,6 +67,8 @@ import { ExtraControlProps, SelectControlConfig, Dataset, + ControlState, + ControlPanelState, } from '../types'; import { ColumnOption } from '../components/ColumnOption'; @@ -544,6 +546,30 @@ const enableExploreDnd = isFeatureEnabled( FeatureFlag.ENABLE_EXPLORE_DRAG_AND_DROP, ); +const x_axis: SharedControlConfig = { + ...(enableExploreDnd ? dndGroupByControl : groupByControl), + label: t('X-axis'), + default: ( + control: ControlState, + controlPanel: Partial, + ) => { + // default to the chosen time column if x-axis is unset and the + // GENERIC_CHART_AXES feature flag is enabled + const { value } = control; + if (value) { + return value; + } + const timeColumn = controlPanel?.form_data?.granularity_sqla; + if (isFeatureEnabled(FeatureFlag.GENERIC_CHART_AXES) && timeColumn) { + return timeColumn; + } + return null; + }, + multi: false, + description: t('Dimension to use on x-axis.'), + validators: [validateNonEmpty], +}; + const sharedControls = { metrics: enableExploreDnd ? dnd_adhoc_metrics : metrics, metric: enableExploreDnd ? dnd_adhoc_metric : metric, @@ -579,6 +605,7 @@ const sharedControls = { series_limit_metric: enableExploreDnd ? dnd_sort_by : sort_by, legacy_order_by: enableExploreDnd ? dnd_sort_by : sort_by, truncate_metric, + x_axis, }; export { sharedControls, dndEntity, dndColumnsControl }; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/types.ts index 005d2a79f0a27..36df083fb8189 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/types.ts @@ -25,7 +25,8 @@ import { SetDataMaskHook, } from '@superset-ui/core'; import { EChartsCoreOption } from 'echarts'; -import { EchartsTitleFormData, DEFAULT_TITLE_FORM_DATA } from '../types'; +import { EchartsTitleFormData } from '../types'; +import { DEFAULT_TITLE_FORM_DATA } from '../constants'; export type BoxPlotQueryFormData = QueryFormData & { numberFormat?: string; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts index 3d0b279cc12fd..aa216981391f0 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts @@ -35,7 +35,6 @@ import { EchartsFunnelLabelTypeType, FunnelChartTransformedProps, } from './types'; -import { DEFAULT_LEGEND_FORM_DATA } from '../types'; import { extractGroupbyLabel, getChartPadding, @@ -43,7 +42,7 @@ import { sanitizeHtml, } from '../utils/series'; import { defaultGrid, defaultTooltip } from '../defaults'; -import { OpacityEnum } from '../constants'; +import { OpacityEnum, DEFAULT_LEGEND_FORM_DATA } from '../constants'; const percentFormatter = getNumberFormatter(NumberFormats.PERCENT_2_POINT); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/types.ts index 398fa40d57fb3..cd392997cf72f 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/types.ts @@ -25,12 +25,8 @@ import { QueryFormData, SetDataMaskHook, } from '@superset-ui/core'; -import { - DEFAULT_LEGEND_FORM_DATA, - EchartsLegendFormData, - LegendOrientation, - LegendType, -} from '../types'; +import { EchartsLegendFormData, LegendOrientation, LegendType } from '../types'; +import { DEFAULT_LEGEND_FORM_DATA } from '../constants'; export type EchartsFunnelFormData = QueryFormData & EchartsLegendFormData & { diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/types.ts index f6a1b09ad6604..7ae2a555951b2 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Gauge/types.ts @@ -22,7 +22,8 @@ import { QueryFormColumn, QueryFormData, } from '@superset-ui/core'; -import { DEFAULT_LEGEND_FORM_DATA, EChartTransformedProps } from '../types'; +import { EChartTransformedProps } from '../types'; +import { DEFAULT_LEGEND_FORM_DATA } from '../constants'; export type AxisTickLineStyle = { width: number; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/types.ts index 9cb35c1304450..19938b4b19857 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Graph/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Graph/types.ts @@ -19,12 +19,8 @@ import { QueryFormData } from '@superset-ui/core'; import { GraphNodeItemOption } from 'echarts/types/src/chart/graph/GraphSeries'; import { SeriesTooltipOption } from 'echarts/types/src/util/types'; -import { - DEFAULT_LEGEND_FORM_DATA, - EchartsLegendFormData, - LegendOrientation, - LegendType, -} from '../types'; +import { EchartsLegendFormData, LegendOrientation, LegendType } from '../types'; +import { DEFAULT_LEGEND_FORM_DATA } from '../constants'; export type EdgeSymbol = 'none' | 'circle' | 'arrow'; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx index fb164f1a26e81..4ff8831800dee 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/controlPanel.tsx @@ -36,7 +36,7 @@ import { import { DEFAULT_FORM_DATA } from './types'; import { EchartsTimeseriesSeriesType } from '../Timeseries/types'; -import { legendSection, richTooltipSection, xAxisControl } from '../controls'; +import { legendSection, richTooltipSection } from '../controls'; const { area, @@ -295,7 +295,7 @@ const config: ControlPanelConfig = { ? { label: t('Shared query fields'), expanded: true, - controlSetRows: [[xAxisControl]], + controlSetRows: [['x_axis']], } : null, createQuerySection(t('Query A'), ''), diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts index 51938436fbb0e..85e4030acd0b6 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts @@ -28,17 +28,17 @@ import { QueryFormColumn, } from '@superset-ui/core'; import { - DEFAULT_LEGEND_FORM_DATA, EchartsLegendFormData, EchartsTitleFormData, - DEFAULT_TITLE_FORM_DATA, StackType, + EchartsTimeseriesContributionType, + EchartsTimeseriesSeriesType, } from '../types'; import { + DEFAULT_LEGEND_FORM_DATA, + DEFAULT_TITLE_FORM_DATA, DEFAULT_FORM_DATA as TIMESERIES_DEFAULTS, - EchartsTimeseriesContributionType, - EchartsTimeseriesSeriesType, -} from '../Timeseries/types'; +} from '../constants'; export type EchartsMixedTimeseriesFormData = QueryFormData & { annotationLayers: AnnotationLayer[]; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts index c0466e6bba562..b2924dc297778 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts @@ -36,7 +36,7 @@ import { EchartsPieLabelType, PieChartTransformedProps, } from './types'; -import { DEFAULT_LEGEND_FORM_DATA } from '../types'; +import { DEFAULT_LEGEND_FORM_DATA, OpacityEnum } from '../constants'; import { extractGroupbyLabel, getChartPadding, @@ -45,7 +45,6 @@ import { sanitizeHtml, } from '../utils/series'; import { defaultGrid, defaultTooltip } from '../defaults'; -import { OpacityEnum } from '../constants'; import { convertInteger } from '../utils/convertInteger'; const percentFormatter = getNumberFormatter(NumberFormats.PERCENT_2_POINT); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/types.ts index c97afd3a7c9bb..302df265f480f 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/types.ts @@ -25,12 +25,8 @@ import { QueryFormData, SetDataMaskHook, } from '@superset-ui/core'; -import { - DEFAULT_LEGEND_FORM_DATA, - EchartsLegendFormData, - LegendOrientation, - LegendType, -} from '../types'; +import { EchartsLegendFormData, LegendOrientation, LegendType } from '../types'; +import { DEFAULT_LEGEND_FORM_DATA } from '../constants'; export type EchartsPieFormData = QueryFormData & EchartsLegendFormData & { diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/transformProps.ts index 01a20b82b436e..7bcb59a0648c4 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/transformProps.ts @@ -36,7 +36,7 @@ import { EchartsRadarLabelType, RadarChartTransformedProps, } from './types'; -import { DEFAULT_LEGEND_FORM_DATA } from '../types'; +import { DEFAULT_LEGEND_FORM_DATA, OpacityEnum } from '../constants'; import { extractGroupbyLabel, getChartPadding, @@ -44,7 +44,6 @@ import { getLegendProps, } from '../utils/series'; import { defaultGrid, defaultTooltip } from '../defaults'; -import { OpacityEnum } from '../constants'; export function formatLabel({ params, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/types.ts index 9b053b6264554..ebe571f621658 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/types.ts @@ -27,12 +27,12 @@ import { SetDataMaskHook, } from '@superset-ui/core'; import { - DEFAULT_LEGEND_FORM_DATA, EchartsLegendFormData, LabelPositionEnum, LegendOrientation, LegendType, } from '../types'; +import { DEFAULT_LEGEND_FORM_DATA } from '../constants'; type RadarColumnConfig = Record; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx index c8922dd11e58b..7301cc26e7a36 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx @@ -17,7 +17,7 @@ * under the License. */ import React from 'react'; -import { FeatureFlag, isFeatureEnabled, t } from '@superset-ui/core'; +import { t } from '@superset-ui/core'; import { ControlPanelConfig, ControlPanelsContainerProps, @@ -28,10 +28,10 @@ import { } from '@superset-ui/chart-controls'; import { - DEFAULT_FORM_DATA, EchartsTimeseriesContributionType, EchartsTimeseriesSeriesType, } from '../types'; +import { DEFAULT_FORM_DATA } from '../constants'; import { legendSection, onlyTotalControl, @@ -62,7 +62,7 @@ const config: ControlPanelConfig = { label: t('Query'), expanded: true, controlSetRows: [ - isFeatureEnabled(FeatureFlag.GENERIC_CHART_AXES) ? [xAxisControl] : [], + [xAxisControl], ['metrics'], ['groupby'], [ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx index 1992f4a45609e..f7c96d4d3660d 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx @@ -17,7 +17,7 @@ * under the License. */ import React from 'react'; -import { FeatureFlag, isFeatureEnabled, t } from '@superset-ui/core'; +import { t } from '@superset-ui/core'; import { ControlPanelConfig, ControlPanelsContainerProps, @@ -31,10 +31,10 @@ import { } from '@superset-ui/chart-controls'; import { - DEFAULT_FORM_DATA, EchartsTimeseriesContributionType, OrientationType, } from '../../types'; +import { DEFAULT_FORM_DATA } from '../../constants'; import { legendSection, richTooltipSection, @@ -269,7 +269,7 @@ const config: ControlPanelConfig = { label: t('Query'), expanded: true, controlSetRows: [ - isFeatureEnabled(FeatureFlag.GENERIC_CHART_AXES) ? [xAxisControl] : [], + [xAxisControl], ['metrics'], ['groupby'], [ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/index.ts index 0ffc09098c70b..2c74e6ac690b5 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/index.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/index.ts @@ -25,15 +25,15 @@ import { isFeatureEnabled, t, } from '@superset-ui/core'; -import buildQuery from '../../buildQuery'; -import controlPanel from './controlPanel'; -import transformProps from '../../transformProps'; -import thumbnail from './images/thumbnail.png'; import { EchartsTimeseriesChartProps, EchartsTimeseriesFormData, EchartsTimeseriesSeriesType, } from '../../types'; +import buildQuery from '../../buildQuery'; +import controlPanel from './controlPanel'; +import transformProps from '../../transformProps'; +import thumbnail from './images/thumbnail.png'; import example1 from './images/Bar1.png'; import example2 from './images/Bar2.png'; import example3 from './images/Bar3.png'; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx similarity index 97% rename from superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/controlPanel.tsx rename to superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx index d039a059c590a..0f78f1ee19110 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx @@ -17,7 +17,7 @@ * under the License. */ import React from 'react'; -import { FeatureFlag, isFeatureEnabled, t } from '@superset-ui/core'; +import { t } from '@superset-ui/core'; import { ControlPanelConfig, ControlPanelsContainerProps, @@ -28,16 +28,16 @@ import { } from '@superset-ui/chart-controls'; import { - DEFAULT_FORM_DATA, EchartsTimeseriesContributionType, EchartsTimeseriesSeriesType, -} from './types'; +} from '../../types'; +import { DEFAULT_FORM_DATA } from '../../constants'; import { legendSection, richTooltipSection, showValueSection, xAxisControl, -} from '../controls'; +} from '../../../controls'; const { area, @@ -61,7 +61,7 @@ const config: ControlPanelConfig = { label: t('Query'), expanded: true, controlSetRows: [ - isFeatureEnabled(FeatureFlag.GENERIC_CHART_AXES) ? [xAxisControl] : [], + [xAxisControl], ['metrics'], ['groupby'], [ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/index.ts index 6f4a780c36fc3..0d89373c95a42 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/index.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/index.ts @@ -25,15 +25,15 @@ import { isFeatureEnabled, t, } from '@superset-ui/core'; -import buildQuery from '../../buildQuery'; -import controlPanel from '../controlPanel'; -import transformProps from '../../transformProps'; -import thumbnail from './images/thumbnail.png'; import { EchartsTimeseriesChartProps, EchartsTimeseriesFormData, EchartsTimeseriesSeriesType, } from '../../types'; +import buildQuery from '../../buildQuery'; +import controlPanel from './controlPanel'; +import transformProps from '../../transformProps'; +import thumbnail from './images/thumbnail.png'; import example1 from './images/Line1.png'; import example2 from './images/Line2.png'; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx index fd2fa79651305..471fe03d1ce03 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx @@ -17,7 +17,7 @@ * under the License. */ import React from 'react'; -import { FeatureFlag, isFeatureEnabled, t } from '@superset-ui/core'; +import { t } from '@superset-ui/core'; import { ControlPanelConfig, ControlPanelsContainerProps, @@ -27,7 +27,7 @@ import { sharedControls, } from '@superset-ui/chart-controls'; -import { DEFAULT_FORM_DATA } from '../../types'; +import { DEFAULT_FORM_DATA } from '../../constants'; import { legendSection, richTooltipSection, @@ -53,7 +53,7 @@ const config: ControlPanelConfig = { label: t('Query'), expanded: true, controlSetRows: [ - isFeatureEnabled(FeatureFlag.GENERIC_CHART_AXES) ? [xAxisControl] : [], + [xAxisControl], ['metrics'], ['groupby'], ['adhoc_filters'], diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/index.ts index 7c77868a58451..fc544bdf73ace 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/index.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/index.ts @@ -25,15 +25,15 @@ import { isFeatureEnabled, t, } from '@superset-ui/core'; -import buildQuery from '../../buildQuery'; -import controlPanel from './controlPanel'; -import transformProps from '../../transformProps'; -import thumbnail from './images/thumbnail.png'; import { EchartsTimeseriesChartProps, EchartsTimeseriesFormData, EchartsTimeseriesSeriesType, } from '../../types'; +import buildQuery from '../../buildQuery'; +import controlPanel from './controlPanel'; +import transformProps from '../../transformProps'; +import thumbnail from './images/thumbnail.png'; import example1 from './images/Scatter1.png'; const scatterTransformProps = (chartProps: EchartsTimeseriesChartProps) => diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/controlPanel.tsx similarity index 96% rename from superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/controlPanel.tsx rename to superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/controlPanel.tsx index 8dc34861b1335..24350aebb6be1 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/controlPanel.tsx @@ -17,7 +17,7 @@ * under the License. */ import React from 'react'; -import { FeatureFlag, isFeatureEnabled, t } from '@superset-ui/core'; +import { t } from '@superset-ui/core'; import { ControlPanelConfig, ControlPanelsContainerProps, @@ -27,13 +27,14 @@ import { sharedControls, } from '@superset-ui/chart-controls'; -import { DEFAULT_FORM_DATA, EchartsTimeseriesContributionType } from '../types'; +import { EchartsTimeseriesContributionType } from '../../types'; +import { DEFAULT_FORM_DATA } from '../../constants'; import { legendSection, richTooltipSection, showValueSectionWithoutStack, xAxisControl, -} from '../../controls'; +} from '../../../controls'; const { contributionMode, @@ -54,7 +55,7 @@ const config: ControlPanelConfig = { label: t('Query'), expanded: true, controlSetRows: [ - isFeatureEnabled(FeatureFlag.GENERIC_CHART_AXES) ? [xAxisControl] : [], + [xAxisControl], ['metrics'], ['groupby'], [ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/index.ts index ee348b272cd45..c1b8ca47b1c07 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/index.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/index.ts @@ -25,15 +25,15 @@ import { isFeatureEnabled, t, } from '@superset-ui/core'; -import buildQuery from '../../buildQuery'; -import controlPanel from '../controlPanel'; -import transformProps from '../../transformProps'; -import thumbnail from './images/thumbnail.png'; import { EchartsTimeseriesChartProps, EchartsTimeseriesFormData, EchartsTimeseriesSeriesType, } from '../../types'; +import buildQuery from '../../buildQuery'; +import controlPanel from './controlPanel'; +import transformProps from '../../transformProps'; +import thumbnail from './images/thumbnail.png'; import example1 from './images/SmoothLine1.png'; const smoothTransformProps = (chartProps: EchartsTimeseriesChartProps) => diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx index 2902937333860..26c97bd59d224 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx @@ -17,7 +17,7 @@ * under the License. */ import React from 'react'; -import { FeatureFlag, isFeatureEnabled, t } from '@superset-ui/core'; +import { t } from '@superset-ui/core'; import { ControlPanelConfig, ControlPanelsContainerProps, @@ -28,10 +28,10 @@ import { } from '@superset-ui/chart-controls'; import { - DEFAULT_FORM_DATA, EchartsTimeseriesContributionType, EchartsTimeseriesSeriesType, -} from '../types'; +} from '../../types'; +import { DEFAULT_FORM_DATA } from '../constants'; import { legendSection, richTooltipSection, @@ -60,7 +60,7 @@ const config: ControlPanelConfig = { label: t('Query'), expanded: true, controlSetRows: [ - isFeatureEnabled(FeatureFlag.GENERIC_CHART_AXES) ? [xAxisControl] : [], + [xAxisControl], ['metrics'], ['groupby'], [ diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/index.ts index 2a24b708419ef..4889233ae0d59 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/index.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/index.ts @@ -25,14 +25,14 @@ import { isFeatureEnabled, t, } from '@superset-ui/core'; +import { + EchartsTimeseriesChartProps, + EchartsTimeseriesFormData, +} from '@superset-ui/plugin-chart-echarts'; import buildQuery from '../buildQuery'; import controlPanel from './controlPanel'; import transformProps from '../transformProps'; import thumbnail from './images/thumbnail.png'; -import { - EchartsTimeseriesChartProps, - EchartsTimeseriesFormData, -} from '../types'; import example1 from './images/Step1.png'; import example2 from './images/Step2.png'; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/constants.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/constants.ts new file mode 100644 index 0000000000000..2590441ef67f6 --- /dev/null +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/constants.ts @@ -0,0 +1,66 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { sections } from '@superset-ui/chart-controls'; +import { + OrientationType, + EchartsTimeseriesSeriesType, + EchartsTimeseriesFormData, +} from './types'; +import { + DEFAULT_LEGEND_FORM_DATA, + DEFAULT_TITLE_FORM_DATA, +} from '../constants'; + +// @ts-ignore +export const DEFAULT_FORM_DATA: EchartsTimeseriesFormData = { + ...DEFAULT_LEGEND_FORM_DATA, + ...DEFAULT_TITLE_FORM_DATA, + annotationLayers: sections.annotationLayers, + area: false, + forecastEnabled: sections.FORECAST_DEFAULT_DATA.forecastEnabled, + forecastInterval: sections.FORECAST_DEFAULT_DATA.forecastInterval, + forecastPeriods: sections.FORECAST_DEFAULT_DATA.forecastPeriods, + forecastSeasonalityDaily: + sections.FORECAST_DEFAULT_DATA.forecastSeasonalityDaily, + forecastSeasonalityWeekly: + sections.FORECAST_DEFAULT_DATA.forecastSeasonalityWeekly, + forecastSeasonalityYearly: + sections.FORECAST_DEFAULT_DATA.forecastSeasonalityYearly, + logAxis: false, + markerEnabled: false, + markerSize: 6, + minorSplitLine: false, + opacity: 0.2, + orderDesc: true, + rowLimit: 10000, + seriesType: EchartsTimeseriesSeriesType.Line, + stack: false, + tooltipTimeFormat: 'smart_date', + truncateYAxis: false, + yAxisBounds: [null, null], + zoomable: false, + richTooltip: true, + xAxisLabelRotation: 0, + emitFilter: false, + groupby: [], + showValue: false, + onlyTotal: false, + percentageThreshold: 0, + orientation: OrientationType.vertical, +}; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/index.ts index 062d741402ff7..cbdd5cb41b1d3 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/index.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/index.ts @@ -26,7 +26,7 @@ import { t, } from '@superset-ui/core'; import buildQuery from './buildQuery'; -import controlPanel from './controlPanel'; +import controlPanel from './Regular/Line/controlPanel'; import transformProps from './transformProps'; import thumbnail from './images/thumbnail.png'; import { diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts index 89d5c1e03b31d..d1aa4e827b449 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts @@ -36,13 +36,13 @@ import { isDerivedSeries } from '@superset-ui/chart-controls'; import { EChartsCoreOption, SeriesOption } from 'echarts'; import { ZRLineType } from 'echarts/types/src/util/types'; import { - DEFAULT_FORM_DATA, EchartsTimeseriesChartProps, EchartsTimeseriesFormData, EchartsTimeseriesSeriesType, TimeseriesChartTransformedProps, OrientationType, } from './types'; +import { DEFAULT_FORM_DATA } from './constants'; import { ForecastSeriesEnum, ForecastValue } from '../types'; import { parseYAxisBound } from '../utils/controls'; import { diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts index d9b7708146df3..bc7d771bac55d 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts @@ -24,13 +24,10 @@ import { QueryFormData, TimeGranularity, } from '@superset-ui/core'; -import { sections } from '@superset-ui/chart-controls'; import { - DEFAULT_LEGEND_FORM_DATA, EchartsLegendFormData, EChartTransformedProps, EchartsTitleFormData, - DEFAULT_TITLE_FORM_DATA, StackType, } from '../types'; @@ -93,44 +90,6 @@ export type EchartsTimeseriesFormData = QueryFormData & { } & EchartsLegendFormData & EchartsTitleFormData; -// @ts-ignore -export const DEFAULT_FORM_DATA: EchartsTimeseriesFormData = { - ...DEFAULT_LEGEND_FORM_DATA, - annotationLayers: sections.annotationLayers, - area: false, - forecastEnabled: sections.FORECAST_DEFAULT_DATA.forecastEnabled, - forecastInterval: sections.FORECAST_DEFAULT_DATA.forecastInterval, - forecastPeriods: sections.FORECAST_DEFAULT_DATA.forecastPeriods, - forecastSeasonalityDaily: - sections.FORECAST_DEFAULT_DATA.forecastSeasonalityDaily, - forecastSeasonalityWeekly: - sections.FORECAST_DEFAULT_DATA.forecastSeasonalityWeekly, - forecastSeasonalityYearly: - sections.FORECAST_DEFAULT_DATA.forecastSeasonalityYearly, - logAxis: false, - markerEnabled: false, - markerSize: 6, - minorSplitLine: false, - opacity: 0.2, - orderDesc: true, - rowLimit: 10000, - seriesType: EchartsTimeseriesSeriesType.Line, - stack: false, - tooltipTimeFormat: 'smart_date', - truncateYAxis: false, - yAxisBounds: [null, null], - zoomable: false, - richTooltip: true, - xAxisLabelRotation: 0, - emitFilter: false, - groupby: [], - showValue: false, - onlyTotal: false, - percentageThreshold: 0, - orientation: OrientationType.vertical, - ...DEFAULT_TITLE_FORM_DATA, -}; - export interface EchartsTimeseriesChartProps extends ChartProps { formData: EchartsTimeseriesFormData; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts b/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts index 513a0bebc1843..7dd823f644792 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/constants.ts @@ -19,7 +19,13 @@ import { JsonValue, t, TimeGranularity } from '@superset-ui/core'; import { ReactNode } from 'react'; -import { LabelPositionEnum } from './types'; +import { + EchartsLegendFormData, + EchartsTitleFormData, + LabelPositionEnum, + LegendOrientation, + LegendType, +} from './types'; // eslint-disable-next-line import/prefer-default-export export const NULL_STRING = ''; @@ -84,3 +90,20 @@ export const TIMEGRAIN_TO_TIMESTAMP = { [TimeGranularity.QUARTER]: 3600 * 1000 * 24 * 31 * 3, [TimeGranularity.YEAR]: 3600 * 1000 * 24 * 31 * 12, }; + +export const DEFAULT_LEGEND_FORM_DATA: EchartsLegendFormData = { + legendMargin: null, + legendOrientation: LegendOrientation.Top, + legendType: LegendType.Scroll, + showLegend: true, +}; + +export const DEFAULT_TITLE_FORM_DATA: EchartsTitleFormData = { + xAxisTitle: '', + xAxisTitleMargin: 0, + yAxisTitle: '', + yAxisTitleMargin: 0, + yAxisTitlePosition: 'Top', +}; + +export { DEFAULT_FORM_DATA } from './Timeseries/constants'; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx index b8d54fc09a41a..d832196b5e572 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx @@ -17,22 +17,15 @@ * under the License. */ import React from 'react'; -import { - FeatureFlag, - isFeatureEnabled, - t, - validateNonEmpty, -} from '@superset-ui/core'; +import { FeatureFlag, isFeatureEnabled, t } from '@superset-ui/core'; import { ControlPanelsContainerProps, - ControlPanelState, ControlSetItem, ControlSetRow, - ControlState, sharedControls, } from '@superset-ui/chart-controls'; -import { DEFAULT_LEGEND_FORM_DATA } from './types'; -import { DEFAULT_FORM_DATA } from './Timeseries/types'; +import { DEFAULT_LEGEND_FORM_DATA } from './constants'; +import { DEFAULT_FORM_DATA } from './Timeseries/constants'; const { legendMargin, legendOrientation, legendType, showLegend } = DEFAULT_LEGEND_FORM_DATA; @@ -145,32 +138,9 @@ export const onlyTotalControl: ControlSetItem = { }, }; -export const xAxisControl: ControlSetItem = { - name: 'x_axis', - config: { - ...sharedControls.groupby, - label: t('X-axis'), - default: ( - control: ControlState, - controlPanel: Partial, - ) => { - // default to the chosen time column if x-axis is unset and the - // GENERIC_CHART_AXES feature flag is enabled - const { value } = control; - if (value) { - return value; - } - const timeColumn = controlPanel?.form_data?.granularity_sqla; - if (isFeatureEnabled(FeatureFlag.GENERIC_CHART_AXES) && timeColumn) { - return timeColumn; - } - return null; - }, - multi: false, - description: t('Dimension to use on x-axis.'), - validators: [validateNonEmpty], - }, -}; +export const xAxisControl = isFeatureEnabled(FeatureFlag.GENERIC_CHART_AXES) + ? 'x_axis' + : null; const percentageThresholdControl: ControlSetItem = { name: 'percentage_threshold', diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/index.ts index 84a1a3a3dcc39..9890eb4c13e89 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/index.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/index.ts @@ -45,7 +45,7 @@ export { default as TimeseriesTransformProps } from './Timeseries/transformProps export { default as TreeTransformProps } from './Tree/transformProps'; export { default as TreemapTransformProps } from './Treemap/transformProps'; -export { DEFAULT_FORM_DATA as TimeseriesDefaultFormData } from './Timeseries/types'; +export { DEFAULT_FORM_DATA as TimeseriesDefaultFormData } from './Timeseries/constants'; export * from './types'; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/types.ts index d84b7079c4794..487c7443415bb 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/types.ts @@ -85,13 +85,6 @@ export type EchartsLegendFormData = { showLegend: boolean; }; -export const DEFAULT_LEGEND_FORM_DATA: EchartsLegendFormData = { - legendMargin: null, - legendOrientation: LegendOrientation.Top, - legendType: LegendType.Scroll, - showLegend: true, -}; - export type EventHandlers = Record; export enum LabelPositionEnum { @@ -132,14 +125,6 @@ export interface EchartsTitleFormData { yAxisTitlePosition: string; } -export const DEFAULT_TITLE_FORM_DATA: EchartsTitleFormData = { - xAxisTitle: '', - xAxisTitleMargin: 0, - yAxisTitle: '', - yAxisTitleMargin: 0, - yAxisTitlePosition: 'Top', -}; - export type StackType = boolean | null | Partial; export * from './Timeseries/types'; diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/BoxPlot/buildQuery.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/BoxPlot/buildQuery.test.ts index 6859248713442..304f5b7065cfc 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/BoxPlot/buildQuery.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/BoxPlot/buildQuery.test.ts @@ -20,7 +20,7 @@ import { isPostProcessingBoxplot, PostProcessingBoxplot, } from '@superset-ui/core'; -import { DEFAULT_TITLE_FORM_DATA } from '../../src/types'; +import { DEFAULT_TITLE_FORM_DATA } from '../../src/constants'; import buildQuery from '../../src/BoxPlot/buildQuery'; import { BoxPlotQueryFormData } from '../../src/BoxPlot/types'; From b7eb23544077aa867bb17694e8a050b868e3b9a2 Mon Sep 17 00:00:00 2001 From: Sam Firke Date: Mon, 20 Jun 2022 11:49:45 -0400 Subject: [PATCH 58/71] style(typo): occured -> occurred (#20116) * Occured -> Occurred * Occured -> Occurred * Occured -> Occurred * Occured - > Occurred * Update FallbackComponent.tsx * Update FallbackComponent.tsx Co-authored-by: John Bodley <4567245+john-bodley@users.noreply.github.com> --- .../superset-ui-core/src/chart/components/FallbackComponent.tsx | 2 +- .../superset-ui-core/test/chart/components/SuperChart.test.tsx | 2 +- superset-frontend/src/dashboard/actions/dashboardState.js | 2 +- superset/translations/nl/LC_MESSAGES/messages.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/superset-frontend/packages/superset-ui-core/src/chart/components/FallbackComponent.tsx b/superset-frontend/packages/superset-ui-core/src/chart/components/FallbackComponent.tsx index f7a360de25abc..5c22f920824e8 100644 --- a/superset-frontend/packages/superset-ui-core/src/chart/components/FallbackComponent.tsx +++ b/superset-frontend/packages/superset-ui-core/src/chart/components/FallbackComponent.tsx @@ -41,7 +41,7 @@ export default function FallbackComponent({ >
- Oops! An error occured! + Oops! An error occurred!
{error ? error.toString() : 'Unknown Error'}
diff --git a/superset-frontend/packages/superset-ui-core/test/chart/components/SuperChart.test.tsx b/superset-frontend/packages/superset-ui-core/test/chart/components/SuperChart.test.tsx index 2b24df3aeedf0..2233250f68ef4 100644 --- a/superset-frontend/packages/superset-ui-core/test/chart/components/SuperChart.test.tsx +++ b/superset-frontend/packages/superset-ui-core/test/chart/components/SuperChart.test.tsx @@ -121,7 +121,7 @@ describe('SuperChart', () => { ); await new Promise(resolve => setImmediate(resolve)); wrapper.update(); - expect(wrapper.text()).toContain('Oops! An error occured!'); + expect(wrapper.text()).toContain('Oops! An error occurred!'); }); it('renders custom FallbackComponent', () => { expectedErrors = 1; diff --git a/superset-frontend/src/dashboard/actions/dashboardState.js b/superset-frontend/src/dashboard/actions/dashboardState.js index 053aa2f15fd49..f96b3ebadf28b 100644 --- a/superset-frontend/src/dashboard/actions/dashboardState.js +++ b/superset-frontend/src/dashboard/actions/dashboardState.js @@ -325,7 +325,7 @@ export function saveDashboardRequest(data, id, saveType) { const onError = async response => { const { error, message } = await getClientErrorObject(response); - let errorText = t('Sorry, an unknown error occured'); + let errorText = t('Sorry, an unknown error occurred'); if (error) { errorText = t( diff --git a/superset/translations/nl/LC_MESSAGES/messages.json b/superset/translations/nl/LC_MESSAGES/messages.json index 699eb84297d9d..f69b2dce23bb2 100644 --- a/superset/translations/nl/LC_MESSAGES/messages.json +++ b/superset/translations/nl/LC_MESSAGES/messages.json @@ -3478,7 +3478,7 @@ "This dashboard was saved successfully.": [ "Dit dashboard is succesvol opgeslagen." ], - "Sorry, an unknown error occured": [""], + "Sorry, an unknown error occurred": [""], "Sorry, there was an error saving this dashboard: %s": [""], "You do not have permission to edit this dashboard": [ "U hebt geen toestemming om dit dashboard te bewerken" From 68af5980ea5ae98978c809f308891e2e27bed220 Mon Sep 17 00:00:00 2001 From: Samira El Aabidi <54845154+Samira-El@users.noreply.github.com> Date: Tue, 21 Jun 2022 04:14:08 +0300 Subject: [PATCH 59/71] feat(chart): Enable caching per user when user impersonation is enabled (#20114) * add username to extra cache keys when impersonation is enabled. * don't put effective_user in extra_cache_key * get_impersonation_key method in engine_spec class to construct an impersonation key * pass datasource when creating query objects * adding an impersonation key when construction cache key * add feature flag to control caching per user * revert changes * make precommit and pylint happy * pass a User instance * remove unnecessary import --- superset/common/query_context_factory.py | 4 +++- superset/common/query_object.py | 20 ++++++++++++++++++++ superset/config.py | 3 +++ superset/db_engine_specs/base.py | 12 ++++++++++++ 4 files changed, 38 insertions(+), 1 deletion(-) diff --git a/superset/common/query_context_factory.py b/superset/common/query_context_factory.py index cb40b9540818c..2056109bbff70 100644 --- a/superset/common/query_context_factory.py +++ b/superset/common/query_context_factory.py @@ -58,7 +58,9 @@ def create( result_type = result_type or ChartDataResultType.FULL result_format = result_format or ChartDataResultFormat.JSON queries_ = [ - self._query_object_factory.create(result_type, **query_obj) + self._query_object_factory.create( + result_type, datasource=datasource, **query_obj + ) for query_obj in queries ] cache_values = { diff --git a/superset/common/query_object.py b/superset/common/query_object.py index 40d37041b9166..a8585fd47e055 100644 --- a/superset/common/query_object.py +++ b/superset/common/query_object.py @@ -23,9 +23,11 @@ from pprint import pformat from typing import Any, Dict, List, NamedTuple, Optional, TYPE_CHECKING +from flask import g from flask_babel import gettext as _ from pandas import DataFrame +from superset import feature_flag_manager from superset.common.chart_data import ChartDataResultType from superset.exceptions import ( InvalidPostProcessingError, @@ -396,6 +398,24 @@ def cache_key(self, **extra: Any) -> str: if annotation_layers: cache_dict["annotation_layers"] = annotation_layers + # Add an impersonation key to cache if impersonation is enabled on the db + if ( + feature_flag_manager.is_feature_enabled("CACHE_IMPERSONATION") + and self.datasource + and hasattr(self.datasource, "database") + and self.datasource.database.impersonate_user + ): + + if key := self.datasource.database.db_engine_spec.get_impersonation_key( + getattr(g, "user", None) + ): + + logger.debug( + "Adding impersonation key to QueryObject cache dict: %s", key + ) + + cache_dict["impersonation_key"] = key + return md5_sha_from_dict(cache_dict, default=json_int_dttm_ser, ignore_nan=True) def exec_post_processing(self, df: DataFrame) -> DataFrame: diff --git a/superset/config.py b/superset/config.py index 8a5ec248fb8a3..17c6a55412db8 100644 --- a/superset/config.py +++ b/superset/config.py @@ -429,6 +429,9 @@ def _try_json_readsha(filepath: str, length: int) -> Optional[str]: # Apply RLS rules to SQL Lab queries. This requires parsing and manipulating the # query, and might break queries and/or allow users to bypass RLS. Use with care! "RLS_IN_SQLLAB": False, + # Enable caching per impersonation key (e.g username) in a datasource where user + # impersonation is enabled + "CACHE_IMPERSONATION": False, } # Feature flags may also be set via 'SUPERSET_FEATURE_' prefixed environment vars. diff --git a/superset/db_engine_specs/base.py b/superset/db_engine_specs/base.py index 6a2ddc5e5c3f4..b4f4ec25c451e 100644 --- a/superset/db_engine_specs/base.py +++ b/superset/db_engine_specs/base.py @@ -41,6 +41,7 @@ from apispec import APISpec from apispec.ext.marshmallow import MarshmallowPlugin from flask import current_app +from flask_appbuilder.security.sqla.models import User from flask_babel import gettext as __, lazy_gettext as _ from marshmallow import fields, Schema from marshmallow.validate import Range @@ -1537,6 +1538,17 @@ def cancel_query( # pylint: disable=unused-argument def parse_sql(cls, sql: str) -> List[str]: return [str(s).strip(" ;") for s in sqlparse.parse(sql)] + @classmethod + def get_impersonation_key(cls, user: Optional[User]) -> Any: + """ + Construct an impersonation key, by default it's the given username. + + :param user: logged in user + + :returns: username if given user is not null + """ + return user.username if user else None + # schema for adding a database by providing parameters instead of the # full SQLAlchemy URI From c79b0d62d01f9b3eef23a0b74a63febd32206055 Mon Sep 17 00:00:00 2001 From: Yongjie Zhao Date: Tue, 21 Jun 2022 11:38:07 +0800 Subject: [PATCH 60/71] refactor: create echarts query section (#20445) --- .../src/sections/echartsTimeSeriesQuery.tsx | 59 +++++++++++++++++++ .../src/sections/index.ts | 1 + .../src/shared-controls/constants.tsx | 48 +++++++++++++++ .../src/shared-controls/dndControls.tsx | 6 ++ .../src/shared-controls/index.tsx | 35 +++-------- .../superset-ui-core/src/query/types/Query.ts | 5 ++ .../src/MixedTimeseries/types.ts | 6 +- .../src/Timeseries/Area/controlPanel.tsx | 41 +------------ .../Timeseries/Regular/Bar/controlPanel.tsx | 41 +------------ .../Timeseries/Regular/Line/controlPanel.tsx | 41 +------------ .../Regular/Scatter/controlPanel.tsx | 19 +----- .../Regular/SmoothLine/controlPanel.tsx | 37 +----------- .../src/Timeseries/Step/controlPanel.tsx | 41 +------------ .../src/Timeseries/types.ts | 8 +-- .../plugin-chart-echarts/src/controls.tsx | 6 +- .../standardizedFormData.test.tsx | 5 +- 16 files changed, 145 insertions(+), 254 deletions(-) create mode 100644 superset-frontend/packages/superset-ui-chart-controls/src/sections/echartsTimeSeriesQuery.tsx create mode 100644 superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/constants.tsx diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/sections/echartsTimeSeriesQuery.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/sections/echartsTimeSeriesQuery.tsx new file mode 100644 index 0000000000000..b10d38ae7ce1e --- /dev/null +++ b/superset-frontend/packages/superset-ui-chart-controls/src/sections/echartsTimeSeriesQuery.tsx @@ -0,0 +1,59 @@ +/** + * 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 { + ContributionType, + FeatureFlag, + isFeatureEnabled, + t, +} from '@superset-ui/core'; +import { ControlPanelSectionConfig } from '../types'; +import { emitFilterControl } from '../shared-controls/emitFilterControl'; + +export const echartsTimeSeriesQuery: ControlPanelSectionConfig = { + label: t('Query'), + expanded: true, + controlSetRows: [ + [isFeatureEnabled(FeatureFlag.GENERIC_CHART_AXES) ? 'x_axis' : null], + ['metrics'], + ['groupby'], + [ + { + name: 'contributionMode', + config: { + type: 'SelectControl', + label: t('Contribution Mode'), + default: null, + choices: [ + [null, 'None'], + [ContributionType.Row, 'Row'], + [ContributionType.Column, 'Series'], + ], + description: t('Calculate contribution per series or row'), + }, + }, + ], + ['adhoc_filters'], + emitFilterControl, + ['limit'], + ['timeseries_limit_metric'], + ['order_desc'], + ['row_limit'], + ['truncate_metric'], + ], +}; diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/sections/index.ts b/superset-frontend/packages/superset-ui-chart-controls/src/sections/index.ts index 2f6496e67ab7b..c0113b189fd8e 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/sections/index.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/sections/index.ts @@ -22,3 +22,4 @@ export * from './advancedAnalytics'; export * from './annotationsAndLayers'; export * from './forecastInterval'; export * from './chartTitle'; +export * from './echartsTimeSeriesQuery'; diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/constants.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/constants.tsx new file mode 100644 index 0000000000000..d8a80f8af2bf9 --- /dev/null +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/constants.tsx @@ -0,0 +1,48 @@ +/** + * 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 { + FeatureFlag, + isFeatureEnabled, + t, + validateNonEmpty, +} from '@superset-ui/core'; +import { ControlPanelState, ControlState } from '../types'; + +export const xAxisControlConfig = { + label: t('X-axis'), + default: ( + control: ControlState, + controlPanel: Partial, + ) => { + // default to the chosen time column if x-axis is unset and the + // GENERIC_CHART_AXES feature flag is enabled + const { value } = control; + if (value) { + return value; + } + const timeColumn = controlPanel?.form_data?.granularity_sqla; + if (isFeatureEnabled(FeatureFlag.GENERIC_CHART_AXES) && timeColumn) { + return timeColumn; + } + return null; + }, + multi: false, + description: t('Dimension to use on x-axis.'), + validators: [validateNonEmpty], +}; diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/dndControls.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/dndControls.tsx index ce63590f740bb..43b0059046ba2 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/dndControls.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/dndControls.tsx @@ -28,6 +28,7 @@ import { import { ExtraControlProps, SharedControlConfig, Dataset } from '../types'; import { DATASET_TIME_COLUMN_OPTION, TIME_FILTER_LABELS } from '../constants'; import { QUERY_TIME_COLUMN_OPTION, defineSavedMetrics } from '..'; +import { xAxisControlConfig } from './constants'; export const dndGroupByControl: SharedControlConfig<'DndColumnSelect'> = { type: 'DndColumnSelect', @@ -222,3 +223,8 @@ export const dnd_granularity_sqla: typeof dndGroupByControl = { }; }, }; + +export const dnd_x_axis: SharedControlConfig<'DndColumnSelect'> = { + ...dndGroupByControl, + ...xAxisControlConfig, +}; diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx index d42d4155555d8..104ac88c0821c 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/index.tsx @@ -67,8 +67,6 @@ import { ExtraControlProps, SelectControlConfig, Dataset, - ControlState, - ControlPanelState, } from '../types'; import { ColumnOption } from '../components/ColumnOption'; @@ -87,8 +85,10 @@ import { dndGroupByControl, dndSeries, dnd_adhoc_metric_2, + dnd_x_axis, } from './dndControls'; import { QUERY_TIME_COLUMN_OPTION } from '..'; +import { xAxisControlConfig } from './constants'; const categoricalSchemeRegistry = getCategoricalSchemeRegistry(); const sequentialSchemeRegistry = getSequentialSchemeRegistry(); @@ -542,34 +542,15 @@ const truncate_metric: SharedControlConfig<'CheckboxControl'> = { description: t('Whether to truncate metrics'), }; +const x_axis: SharedControlConfig<'SelectControl', ColumnMeta> = { + ...groupByControl, + ...xAxisControlConfig, +}; + const enableExploreDnd = isFeatureEnabled( FeatureFlag.ENABLE_EXPLORE_DRAG_AND_DROP, ); -const x_axis: SharedControlConfig = { - ...(enableExploreDnd ? dndGroupByControl : groupByControl), - label: t('X-axis'), - default: ( - control: ControlState, - controlPanel: Partial, - ) => { - // default to the chosen time column if x-axis is unset and the - // GENERIC_CHART_AXES feature flag is enabled - const { value } = control; - if (value) { - return value; - } - const timeColumn = controlPanel?.form_data?.granularity_sqla; - if (isFeatureEnabled(FeatureFlag.GENERIC_CHART_AXES) && timeColumn) { - return timeColumn; - } - return null; - }, - multi: false, - description: t('Dimension to use on x-axis.'), - validators: [validateNonEmpty], -}; - const sharedControls = { metrics: enableExploreDnd ? dnd_adhoc_metrics : metrics, metric: enableExploreDnd ? dnd_adhoc_metric : metric, @@ -605,7 +586,7 @@ const sharedControls = { series_limit_metric: enableExploreDnd ? dnd_sort_by : sort_by, legacy_order_by: enableExploreDnd ? dnd_sort_by : sort_by, truncate_metric, - x_axis, + x_axis: enableExploreDnd ? dnd_x_axis : x_axis, }; export { sharedControls, dndEntity, dndColumnsControl }; diff --git a/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts b/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts index bcbedf536bc0b..d622f1ed984e4 100644 --- a/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts +++ b/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts @@ -375,4 +375,9 @@ export const testQuery: Query = { ], }; +export enum ContributionType { + Row = 'row', + Column = 'column', +} + export default {}; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts index 85e4030acd0b6..2cef5cd681a9d 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts @@ -26,12 +26,12 @@ import { ChartProps, ChartDataResponseResult, QueryFormColumn, + ContributionType, } from '@superset-ui/core'; import { EchartsLegendFormData, EchartsTitleFormData, StackType, - EchartsTimeseriesContributionType, EchartsTimeseriesSeriesType, } from '../types'; import { @@ -63,8 +63,8 @@ export type EchartsMixedTimeseriesFormData = QueryFormData & { // types specific to Query A and Query B area: boolean; areaB: boolean; - contributionMode?: EchartsTimeseriesContributionType; - contributionModeB?: EchartsTimeseriesContributionType; + contributionMode?: ContributionType; + contributionModeB?: ContributionType; markerEnabled: boolean; markerEnabledB: boolean; markerSize: number; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx index 7301cc26e7a36..751191c4df674 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Area/controlPanel.tsx @@ -22,27 +22,21 @@ import { ControlPanelConfig, ControlPanelsContainerProps, D3_TIME_FORMAT_DOCS, - emitFilterControl, sections, sharedControls, } from '@superset-ui/chart-controls'; -import { - EchartsTimeseriesContributionType, - EchartsTimeseriesSeriesType, -} from '../types'; +import { EchartsTimeseriesSeriesType } from '../types'; import { DEFAULT_FORM_DATA } from '../constants'; import { legendSection, onlyTotalControl, showValueControl, richTooltipSection, - xAxisControl, } from '../../controls'; import { AreaChartExtraControlsOptions } from '../../constants'; const { - contributionMode, logAxis, markerEnabled, markerSize, @@ -58,38 +52,7 @@ const { const config: ControlPanelConfig = { controlPanelSections: [ sections.legacyTimeseriesTime, - { - label: t('Query'), - expanded: true, - controlSetRows: [ - [xAxisControl], - ['metrics'], - ['groupby'], - [ - { - name: 'contributionMode', - config: { - type: 'SelectControl', - label: t('Contribution Mode'), - default: contributionMode, - choices: [ - [null, 'None'], - [EchartsTimeseriesContributionType.Row, 'Row'], - [EchartsTimeseriesContributionType.Column, 'Series'], - ], - description: t('Calculate contribution per series or row'), - }, - }, - ], - ['adhoc_filters'], - emitFilterControl, - ['limit'], - ['timeseries_limit_metric'], - ['order_desc'], - ['row_limit'], - ['truncate_metric'], - ], - }, + sections.echartsTimeSeriesQuery, sections.advancedAnalyticsControls, sections.annotationsAndLayersControls, sections.forecastIntervalControls, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx index f7c96d4d3660d..85d631d719726 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Bar/controlPanel.tsx @@ -24,26 +24,20 @@ import { ControlSetRow, ControlStateMapping, D3_TIME_FORMAT_DOCS, - emitFilterControl, formatSelectOptions, sections, sharedControls, } from '@superset-ui/chart-controls'; -import { - EchartsTimeseriesContributionType, - OrientationType, -} from '../../types'; +import { OrientationType } from '../../types'; import { DEFAULT_FORM_DATA } from '../../constants'; import { legendSection, richTooltipSection, showValueSection, - xAxisControl, } from '../../../controls'; const { - contributionMode, logAxis, minorSplitLine, rowLimit, @@ -265,38 +259,7 @@ function createAxisControl(axis: 'x' | 'y'): ControlSetRow[] { const config: ControlPanelConfig = { controlPanelSections: [ sections.legacyTimeseriesTime, - { - label: t('Query'), - expanded: true, - controlSetRows: [ - [xAxisControl], - ['metrics'], - ['groupby'], - [ - { - name: 'contributionMode', - config: { - type: 'SelectControl', - label: t('Contribution Mode'), - default: contributionMode, - choices: [ - [null, 'None'], - [EchartsTimeseriesContributionType.Row, 'Row'], - [EchartsTimeseriesContributionType.Column, 'Series'], - ], - description: t('Calculate contribution per series or row'), - }, - }, - ], - ['adhoc_filters'], - emitFilterControl, - ['limit'], - ['timeseries_limit_metric'], - ['order_desc'], - ['row_limit'], - ['truncate_metric'], - ], - }, + sections.echartsTimeSeriesQuery, sections.advancedAnalyticsControls, sections.annotationsAndLayersControls, sections.forecastIntervalControls, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx index 0f78f1ee19110..c12d516836b96 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Line/controlPanel.tsx @@ -24,24 +24,18 @@ import { D3_TIME_FORMAT_DOCS, sections, sharedControls, - emitFilterControl, } from '@superset-ui/chart-controls'; -import { - EchartsTimeseriesContributionType, - EchartsTimeseriesSeriesType, -} from '../../types'; +import { EchartsTimeseriesSeriesType } from '../../types'; import { DEFAULT_FORM_DATA } from '../../constants'; import { legendSection, richTooltipSection, showValueSection, - xAxisControl, } from '../../../controls'; const { area, - contributionMode, logAxis, markerEnabled, markerSize, @@ -57,38 +51,7 @@ const { const config: ControlPanelConfig = { controlPanelSections: [ sections.legacyTimeseriesTime, - { - label: t('Query'), - expanded: true, - controlSetRows: [ - [xAxisControl], - ['metrics'], - ['groupby'], - [ - { - name: 'contributionMode', - config: { - type: 'SelectControl', - label: t('Contribution Mode'), - default: contributionMode, - choices: [ - [null, 'None'], - [EchartsTimeseriesContributionType.Row, 'Row'], - [EchartsTimeseriesContributionType.Column, 'Series'], - ], - description: t('Calculate contribution per series or row'), - }, - }, - ], - ['adhoc_filters'], - emitFilterControl, - ['limit'], - ['timeseries_limit_metric'], - ['order_desc'], - ['row_limit'], - ['truncate_metric'], - ], - }, + sections.echartsTimeSeriesQuery, sections.advancedAnalyticsControls, sections.annotationsAndLayersControls, sections.forecastIntervalControls, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx index 471fe03d1ce03..2d65d35dea3d8 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/Scatter/controlPanel.tsx @@ -22,7 +22,6 @@ import { ControlPanelConfig, ControlPanelsContainerProps, D3_TIME_FORMAT_DOCS, - emitFilterControl, sections, sharedControls, } from '@superset-ui/chart-controls'; @@ -32,7 +31,6 @@ import { legendSection, richTooltipSection, showValueSection, - xAxisControl, } from '../../../controls'; const { @@ -49,22 +47,7 @@ const { const config: ControlPanelConfig = { controlPanelSections: [ sections.legacyTimeseriesTime, - { - label: t('Query'), - expanded: true, - controlSetRows: [ - [xAxisControl], - ['metrics'], - ['groupby'], - ['adhoc_filters'], - emitFilterControl, - ['limit'], - ['timeseries_limit_metric'], - ['order_desc'], - ['row_limit'], - ['truncate_metric'], - ], - }, + sections.echartsTimeSeriesQuery, sections.advancedAnalyticsControls, sections.annotationsAndLayersControls, sections.forecastIntervalControls, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/controlPanel.tsx index 24350aebb6be1..a0e6f2c40c3bb 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Regular/SmoothLine/controlPanel.tsx @@ -22,22 +22,18 @@ import { ControlPanelConfig, ControlPanelsContainerProps, D3_TIME_FORMAT_DOCS, - emitFilterControl, sections, sharedControls, } from '@superset-ui/chart-controls'; -import { EchartsTimeseriesContributionType } from '../../types'; import { DEFAULT_FORM_DATA } from '../../constants'; import { legendSection, richTooltipSection, showValueSectionWithoutStack, - xAxisControl, } from '../../../controls'; const { - contributionMode, logAxis, markerEnabled, markerSize, @@ -51,38 +47,7 @@ const { const config: ControlPanelConfig = { controlPanelSections: [ sections.legacyTimeseriesTime, - { - label: t('Query'), - expanded: true, - controlSetRows: [ - [xAxisControl], - ['metrics'], - ['groupby'], - [ - { - name: 'contributionMode', - config: { - type: 'SelectControl', - label: t('Contribution Mode'), - default: contributionMode, - choices: [ - [null, 'None'], - [EchartsTimeseriesContributionType.Row, 'Row'], - [EchartsTimeseriesContributionType.Column, 'Series'], - ], - description: t('Calculate contribution per series or row'), - }, - }, - ], - ['adhoc_filters'], - emitFilterControl, - ['limit'], - ['timeseries_limit_metric'], - ['order_desc'], - ['row_limit'], - ['truncate_metric'], - ], - }, + sections.echartsTimeSeriesQuery, sections.advancedAnalyticsControls, sections.annotationsAndLayersControls, sections.forecastIntervalControls, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx index 26c97bd59d224..1beac8c235859 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/Step/controlPanel.tsx @@ -24,24 +24,18 @@ import { D3_TIME_FORMAT_DOCS, sections, sharedControls, - emitFilterControl, } from '@superset-ui/chart-controls'; -import { - EchartsTimeseriesContributionType, - EchartsTimeseriesSeriesType, -} from '../../types'; +import { EchartsTimeseriesSeriesType } from '../../types'; import { DEFAULT_FORM_DATA } from '../constants'; import { legendSection, richTooltipSection, showValueSection, - xAxisControl, } from '../../controls'; const { area, - contributionMode, logAxis, markerEnabled, markerSize, @@ -56,38 +50,7 @@ const { const config: ControlPanelConfig = { controlPanelSections: [ sections.legacyTimeseriesTime, - { - label: t('Query'), - expanded: true, - controlSetRows: [ - [xAxisControl], - ['metrics'], - ['groupby'], - [ - { - name: 'contributionMode', - config: { - type: 'SelectControl', - label: t('Contribution Mode'), - default: contributionMode, - choices: [ - [null, 'None'], - [EchartsTimeseriesContributionType.Row, 'Row'], - [EchartsTimeseriesContributionType.Column, 'Series'], - ], - description: t('Calculate contribution per series or row'), - }, - }, - ], - ['adhoc_filters'], - emitFilterControl, - ['limit'], - ['timeseries_limit_metric'], - ['order_desc'], - ['row_limit'], - ['truncate_metric'], - ], - }, + sections.echartsTimeSeriesQuery, sections.advancedAnalyticsControls, sections.annotationsAndLayersControls, sections.forecastIntervalControls, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts index bc7d771bac55d..946d41ec164d8 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts @@ -23,6 +23,7 @@ import { QueryFormColumn, QueryFormData, TimeGranularity, + ContributionType, } from '@superset-ui/core'; import { EchartsLegendFormData, @@ -31,11 +32,6 @@ import { StackType, } from '../types'; -export enum EchartsTimeseriesContributionType { - Row = 'row', - Column = 'column', -} - export enum OrientationType { vertical = 'vertical', horizontal = 'horizontal', @@ -55,7 +51,7 @@ export type EchartsTimeseriesFormData = QueryFormData & { annotationLayers: AnnotationLayer[]; area: boolean; colorScheme?: string; - contributionMode?: EchartsTimeseriesContributionType; + contributionMode?: ContributionType; forecastEnabled: boolean; forecastPeriods: number; forecastInterval: number; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx index d832196b5e572..38eee33e74592 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx @@ -17,7 +17,7 @@ * under the License. */ import React from 'react'; -import { FeatureFlag, isFeatureEnabled, t } from '@superset-ui/core'; +import { t } from '@superset-ui/core'; import { ControlPanelsContainerProps, ControlSetItem, @@ -138,10 +138,6 @@ export const onlyTotalControl: ControlSetItem = { }, }; -export const xAxisControl = isFeatureEnabled(FeatureFlag.GENERIC_CHART_AXES) - ? 'x_axis' - : null; - const percentageThresholdControl: ControlSetItem = { name: 'percentage_threshold', config: { diff --git a/superset-frontend/src/explore/controlUtils/standardizedFormData.test.tsx b/superset-frontend/src/explore/controlUtils/standardizedFormData.test.tsx index e048c044983a8..e00bc58e8d0b0 100644 --- a/superset-frontend/src/explore/controlUtils/standardizedFormData.test.tsx +++ b/superset-frontend/src/explore/controlUtils/standardizedFormData.test.tsx @@ -25,7 +25,6 @@ import { sharedControls, publicControls, } from './standardizedFormData'; -import { xAxisControl } from '../../../plugins/plugin-chart-echarts/src/controls'; describe('should collect control values and create SFD', () => { const sharedControlsFormData = {}; @@ -66,7 +65,7 @@ describe('should collect control values and create SFD', () => { }, { label: 'axis column', - controlSetRows: [[xAxisControl]], + controlSetRows: [['x_axis']], }, ], }); @@ -79,7 +78,7 @@ describe('should collect control values and create SFD', () => { }, { label: 'axis column', - controlSetRows: [[xAxisControl]], + controlSetRows: [['x_axis']], }, ], denormalizeFormData: (formData: QueryFormData) => ({ From e3e37cb68f4bb2118580f666cd77bb14ac7ff072 Mon Sep 17 00:00:00 2001 From: "Hugh A. Miles II" Date: Tue, 21 Jun 2022 13:22:39 +0200 Subject: [PATCH 61/71] chore: switching out ConnectorRegistry references for DatasourceDAO (#20380) * rename and move dao file * Update dao.py * add cachekey * Update __init__.py * change reference in query context test * add utils ref * more ref changes * add helpers * add todo in dashboard.py * add cachekey * circular import error in dar.py * push rest of refs * fix linting * fix more linting * update enum * remove references for connector registry * big reafctor * take value * fix * test to see if removing value works * delete connectregistry * address concerns * address comments * fix merge conflicts * address concern II * address concern II * fix test Co-authored-by: Phillip Kelley-Dotson --- superset/__init__.py | 1 - superset/cachekeys/api.py | 6 +- superset/commands/utils.py | 11 +- superset/common/query_context_factory.py | 10 +- superset/common/query_object_factory.py | 16 +- superset/connectors/connector_registry.py | 164 ------------------ superset/connectors/sqla/models.py | 43 +++++ superset/dao/datasource/dao.py | 147 ---------------- superset/dao/exceptions.py | 1 + superset/dashboards/commands/importers/v0.py | 13 +- superset/datasource/__init__.py | 16 ++ superset/datasource/dao.py | 62 +++++++ superset/examples/helpers.py | 4 +- superset/explore/form_data/commands/create.py | 3 +- superset/explore/form_data/commands/state.py | 4 +- superset/explore/form_data/commands/update.py | 3 +- superset/initialization/__init__.py | 7 +- superset/models/dashboard.py | 15 +- superset/models/datasource_access_request.py | 6 +- superset/models/slice.py | 7 +- superset/security/manager.py | 38 ++-- superset/views/core.py | 45 +++-- superset/views/datasource/views.py | 21 ++- superset/views/utils.py | 9 +- tests/integration_tests/access_tests.py | 50 +----- tests/integration_tests/dashboard_utils.py | 8 +- tests/integration_tests/datasource_tests.py | 37 ++-- .../explore/form_data/api_tests.py | 3 +- .../fixtures/birth_names_dashboard.py | 7 +- .../fixtures/energy_dashboard.py | 1 - tests/integration_tests/insert_chart_mixin.py | 7 +- .../integration_tests/query_context_tests.py | 21 ++- tests/integration_tests/security_tests.py | 9 +- .../dao_tests.py} | 41 ++++- 34 files changed, 333 insertions(+), 503 deletions(-) delete mode 100644 superset/connectors/connector_registry.py delete mode 100644 superset/dao/datasource/dao.py create mode 100644 superset/datasource/__init__.py create mode 100644 superset/datasource/dao.py rename tests/unit_tests/{dao/datasource_test.py => datasource/dao_tests.py} (81%) diff --git a/superset/__init__.py b/superset/__init__.py index 6df897f3ecdb1..5c8ff3ca2dc57 100644 --- a/superset/__init__.py +++ b/superset/__init__.py @@ -19,7 +19,6 @@ from werkzeug.local import LocalProxy from superset.app import create_app -from superset.connectors.connector_registry import ConnectorRegistry from superset.extensions import ( appbuilder, cache_manager, diff --git a/superset/cachekeys/api.py b/superset/cachekeys/api.py index 6eb0d54d9eef0..e686c0a6df9e7 100644 --- a/superset/cachekeys/api.py +++ b/superset/cachekeys/api.py @@ -25,7 +25,7 @@ from sqlalchemy.exc import SQLAlchemyError from superset.cachekeys.schemas import CacheInvalidationRequestSchema -from superset.connectors.connector_registry import ConnectorRegistry +from superset.connectors.sqla.models import SqlaTable from superset.extensions import cache_manager, db, event_logger from superset.models.cache import CacheKey from superset.views.base_api import BaseSupersetModelRestApi, statsd_metrics @@ -83,13 +83,13 @@ def invalidate(self) -> Response: return self.response_400(message=str(error)) datasource_uids = set(datasources.get("datasource_uids", [])) for ds in datasources.get("datasources", []): - ds_obj = ConnectorRegistry.get_datasource_by_name( + ds_obj = SqlaTable.get_datasource_by_name( session=db.session, - datasource_type=ds.get("datasource_type"), datasource_name=ds.get("datasource_name"), schema=ds.get("schema"), database_name=ds.get("database_name"), ) + if ds_obj: datasource_uids.add(ds_obj.uid) diff --git a/superset/commands/utils.py b/superset/commands/utils.py index f7564b3de7689..0be5c52e31fd7 100644 --- a/superset/commands/utils.py +++ b/superset/commands/utils.py @@ -25,9 +25,10 @@ OwnersNotFoundValidationError, RolesNotFoundValidationError, ) -from superset.connectors.connector_registry import ConnectorRegistry -from superset.datasets.commands.exceptions import DatasetNotFoundError +from superset.dao.exceptions import DatasourceNotFound +from superset.datasource.dao import DatasourceDAO from superset.extensions import db, security_manager +from superset.utils.core import DatasourceType if TYPE_CHECKING: from superset.connectors.base.models import BaseDatasource @@ -79,8 +80,8 @@ def populate_roles(role_ids: Optional[List[int]] = None) -> List[Role]: def get_datasource_by_id(datasource_id: int, datasource_type: str) -> BaseDatasource: try: - return ConnectorRegistry.get_datasource( - datasource_type, datasource_id, db.session + return DatasourceDAO.get_datasource( + db.session, DatasourceType(datasource_type), datasource_id ) - except DatasetNotFoundError as ex: + except DatasourceNotFound as ex: raise DatasourceNotFoundValidationError() from ex diff --git a/superset/common/query_context_factory.py b/superset/common/query_context_factory.py index 2056109bbff70..1e1d16985ad43 100644 --- a/superset/common/query_context_factory.py +++ b/superset/common/query_context_factory.py @@ -22,8 +22,8 @@ from superset.common.chart_data import ChartDataResultFormat, ChartDataResultType from superset.common.query_context import QueryContext from superset.common.query_object_factory import QueryObjectFactory -from superset.connectors.connector_registry import ConnectorRegistry -from superset.utils.core import DatasourceDict +from superset.datasource.dao import DatasourceDAO +from superset.utils.core import DatasourceDict, DatasourceType if TYPE_CHECKING: from superset.connectors.base.models import BaseDatasource @@ -32,7 +32,7 @@ def create_query_object_factory() -> QueryObjectFactory: - return QueryObjectFactory(config, ConnectorRegistry(), db.session) + return QueryObjectFactory(config, DatasourceDAO(), db.session) class QueryContextFactory: # pylint: disable=too-few-public-methods @@ -82,6 +82,6 @@ def create( # pylint: disable=no-self-use def _convert_to_model(self, datasource: DatasourceDict) -> BaseDatasource: - return ConnectorRegistry.get_datasource( - str(datasource["type"]), int(datasource["id"]), db.session + return DatasourceDAO.get_datasource( + db.session, DatasourceType(datasource["type"]), int(datasource["id"]) ) diff --git a/superset/common/query_object_factory.py b/superset/common/query_object_factory.py index 64ae99deebabc..e9f5122975b52 100644 --- a/superset/common/query_object_factory.py +++ b/superset/common/query_object_factory.py @@ -21,29 +21,29 @@ from superset.common.chart_data import ChartDataResultType from superset.common.query_object import QueryObject -from superset.utils.core import apply_max_row_limit, DatasourceDict +from superset.utils.core import apply_max_row_limit, DatasourceDict, DatasourceType from superset.utils.date_parser import get_since_until if TYPE_CHECKING: from sqlalchemy.orm import sessionmaker - from superset import ConnectorRegistry from superset.connectors.base.models import BaseDatasource + from superset.datasource.dao import DatasourceDAO class QueryObjectFactory: # pylint: disable=too-few-public-methods _config: Dict[str, Any] - _connector_registry: ConnectorRegistry + _datasource_dao: DatasourceDAO _session_maker: sessionmaker def __init__( self, app_configurations: Dict[str, Any], - connector_registry: ConnectorRegistry, + _datasource_dao: DatasourceDAO, session_maker: sessionmaker, ): self._config = app_configurations - self._connector_registry = connector_registry + self._datasource_dao = _datasource_dao self._session_maker = session_maker def create( # pylint: disable=too-many-arguments @@ -75,8 +75,10 @@ def create( # pylint: disable=too-many-arguments ) def _convert_to_model(self, datasource: DatasourceDict) -> BaseDatasource: - return self._connector_registry.get_datasource( - str(datasource["type"]), int(datasource["id"]), self._session_maker() + return self._datasource_dao.get_datasource( + datasource_type=DatasourceType(datasource["type"]), + datasource_id=int(datasource["id"]), + session=self._session_maker(), ) def _process_extras( # pylint: disable=no-self-use diff --git a/superset/connectors/connector_registry.py b/superset/connectors/connector_registry.py deleted file mode 100644 index 06816fa53049f..0000000000000 --- a/superset/connectors/connector_registry.py +++ /dev/null @@ -1,164 +0,0 @@ -# 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. -from typing import Dict, List, Optional, Set, Type, TYPE_CHECKING - -from flask_babel import _ -from sqlalchemy import or_ -from sqlalchemy.orm import Session, subqueryload -from sqlalchemy.orm.exc import NoResultFound - -from superset.datasets.commands.exceptions import DatasetNotFoundError - -if TYPE_CHECKING: - from collections import OrderedDict - - from superset.connectors.base.models import BaseDatasource - from superset.models.core import Database - - -class ConnectorRegistry: - """Central Registry for all available datasource engines""" - - sources: Dict[str, Type["BaseDatasource"]] = {} - - @classmethod - def register_sources(cls, datasource_config: "OrderedDict[str, List[str]]") -> None: - for module_name, class_names in datasource_config.items(): - class_names = [str(s) for s in class_names] - module_obj = __import__(module_name, fromlist=class_names) - for class_name in class_names: - source_class = getattr(module_obj, class_name) - cls.sources[source_class.type] = source_class - - @classmethod - def get_datasource( - cls, datasource_type: str, datasource_id: int, session: Session - ) -> "BaseDatasource": - """Safely get a datasource instance, raises `DatasetNotFoundError` if - `datasource_type` is not registered or `datasource_id` does not - exist.""" - if datasource_type not in cls.sources: - raise DatasetNotFoundError() - - datasource = ( - session.query(cls.sources[datasource_type]) - .filter_by(id=datasource_id) - .one_or_none() - ) - - if not datasource: - raise DatasetNotFoundError() - - return datasource - - @classmethod - def get_all_datasources(cls, session: Session) -> List["BaseDatasource"]: - datasources: List["BaseDatasource"] = [] - for source_class in ConnectorRegistry.sources.values(): - qry = session.query(source_class) - qry = source_class.default_query(qry) - datasources.extend(qry.all()) - return datasources - - @classmethod - def get_datasource_by_id( - cls, session: Session, datasource_id: int - ) -> "BaseDatasource": - """ - Find a datasource instance based on the unique id. - - :param session: Session to use - :param datasource_id: unique id of datasource - :return: Datasource corresponding to the id - :raises NoResultFound: if no datasource is found corresponding to the id - """ - for datasource_class in ConnectorRegistry.sources.values(): - try: - return ( - session.query(datasource_class) - .filter(datasource_class.id == datasource_id) - .one() - ) - except NoResultFound: - # proceed to next datasource type - pass - raise NoResultFound(_("Datasource id not found: %(id)s", id=datasource_id)) - - @classmethod - def get_datasource_by_name( # pylint: disable=too-many-arguments - cls, - session: Session, - datasource_type: str, - datasource_name: str, - schema: str, - database_name: str, - ) -> Optional["BaseDatasource"]: - datasource_class = ConnectorRegistry.sources[datasource_type] - return datasource_class.get_datasource_by_name( - session, datasource_name, schema, database_name - ) - - @classmethod - def query_datasources_by_permissions( # pylint: disable=invalid-name - cls, - session: Session, - database: "Database", - permissions: Set[str], - schema_perms: Set[str], - ) -> List["BaseDatasource"]: - # TODO(bogdan): add unit test - datasource_class = ConnectorRegistry.sources[database.type] - return ( - session.query(datasource_class) - .filter_by(database_id=database.id) - .filter( - or_( - datasource_class.perm.in_(permissions), - datasource_class.schema_perm.in_(schema_perms), - ) - ) - .all() - ) - - @classmethod - def get_eager_datasource( - cls, session: Session, datasource_type: str, datasource_id: int - ) -> "BaseDatasource": - """Returns datasource with columns and metrics.""" - datasource_class = ConnectorRegistry.sources[datasource_type] - return ( - session.query(datasource_class) - .options( - subqueryload(datasource_class.columns), - subqueryload(datasource_class.metrics), - ) - .filter_by(id=datasource_id) - .one() - ) - - @classmethod - def query_datasources_by_name( - cls, - session: Session, - database: "Database", - datasource_name: str, - schema: Optional[str] = None, - ) -> List["BaseDatasource"]: - datasource_class = ConnectorRegistry.sources[database.type] - return datasource_class.query_datasources_by_name( - session, database, datasource_name, schema=schema - ) diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py index ff90cb2a56fef..57730cc711898 100644 --- a/superset/connectors/sqla/models.py +++ b/superset/connectors/sqla/models.py @@ -31,6 +31,7 @@ List, NamedTuple, Optional, + Set, Tuple, Type, Union, @@ -1990,6 +1991,48 @@ def query_datasources_by_name( query = query.filter_by(schema=schema) return query.all() + @classmethod + def query_datasources_by_permissions( # pylint: disable=invalid-name + cls, + session: Session, + database: Database, + permissions: Set[str], + schema_perms: Set[str], + ) -> List["SqlaTable"]: + # TODO(hughhhh): add unit test + return ( + session.query(cls) + .filter_by(database_id=database.id) + .filter( + or_( + SqlaTable.perm.in_(permissions), + SqlaTable.schema_perm.in_(schema_perms), + ) + ) + .all() + ) + + @classmethod + def get_eager_sqlatable_datasource( + cls, session: Session, datasource_id: int + ) -> "SqlaTable": + """Returns SqlaTable with columns and metrics.""" + return ( + session.query(cls) + .options( + sa.orm.subqueryload(cls.columns), + sa.orm.subqueryload(cls.metrics), + ) + .filter_by(id=datasource_id) + .one() + ) + + @classmethod + def get_all_datasources(cls, session: Session) -> List["SqlaTable"]: + qry = session.query(cls) + qry = cls.default_query(qry) + return qry.all() + @staticmethod def default_query(qry: Query) -> Query: return qry.filter_by(is_sqllab_view=False) diff --git a/superset/dao/datasource/dao.py b/superset/dao/datasource/dao.py deleted file mode 100644 index caa45564aa250..0000000000000 --- a/superset/dao/datasource/dao.py +++ /dev/null @@ -1,147 +0,0 @@ -# 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. - -from enum import Enum -from typing import Any, Dict, List, Optional, Set, Type, Union - -from flask_babel import _ -from sqlalchemy import or_ -from sqlalchemy.orm import Session, subqueryload -from sqlalchemy.orm.exc import NoResultFound - -from superset.connectors.sqla.models import SqlaTable -from superset.dao.base import BaseDAO -from superset.dao.exceptions import DatasourceNotFound, DatasourceTypeNotSupportedError -from superset.datasets.commands.exceptions import DatasetNotFoundError -from superset.datasets.models import Dataset -from superset.models.core import Database -from superset.models.sql_lab import Query, SavedQuery -from superset.tables.models import Table -from superset.utils.core import DatasourceType - -Datasource = Union[Dataset, SqlaTable, Table, Query, SavedQuery] - - -class DatasourceDAO(BaseDAO): - - sources: Dict[DatasourceType, Type[Datasource]] = { - DatasourceType.TABLE: SqlaTable, - DatasourceType.QUERY: Query, - DatasourceType.SAVEDQUERY: SavedQuery, - DatasourceType.DATASET: Dataset, - DatasourceType.SLTABLE: Table, - } - - @classmethod - def get_datasource( - cls, session: Session, datasource_type: DatasourceType, datasource_id: int - ) -> Datasource: - if datasource_type not in cls.sources: - raise DatasourceTypeNotSupportedError() - - datasource = ( - session.query(cls.sources[datasource_type]) - .filter_by(id=datasource_id) - .one_or_none() - ) - - if not datasource: - raise DatasourceNotFound() - - return datasource - - @classmethod - def get_all_sqlatables_datasources(cls, session: Session) -> List[Datasource]: - source_class = DatasourceDAO.sources[DatasourceType.TABLE] - qry = session.query(source_class) - qry = source_class.default_query(qry) - return qry.all() - - @classmethod - def get_datasource_by_name( # pylint: disable=too-many-arguments - cls, - session: Session, - datasource_type: DatasourceType, - datasource_name: str, - database_name: str, - schema: str, - ) -> Optional[Datasource]: - datasource_class = DatasourceDAO.sources[datasource_type] - if isinstance(datasource_class, SqlaTable): - return datasource_class.get_datasource_by_name( - session, datasource_name, schema, database_name - ) - return None - - @classmethod - def query_datasources_by_permissions( # pylint: disable=invalid-name - cls, - session: Session, - database: Database, - permissions: Set[str], - schema_perms: Set[str], - ) -> List[Datasource]: - # TODO(hughhhh): add unit test - datasource_class = DatasourceDAO.sources[DatasourceType[database.type]] - if not isinstance(datasource_class, SqlaTable): - return [] - - return ( - session.query(datasource_class) - .filter_by(database_id=database.id) - .filter( - or_( - datasource_class.perm.in_(permissions), - datasource_class.schema_perm.in_(schema_perms), - ) - ) - .all() - ) - - @classmethod - def get_eager_datasource( - cls, session: Session, datasource_type: str, datasource_id: int - ) -> Optional[Datasource]: - """Returns datasource with columns and metrics.""" - datasource_class = DatasourceDAO.sources[DatasourceType[datasource_type]] - if not isinstance(datasource_class, SqlaTable): - return None - return ( - session.query(datasource_class) - .options( - subqueryload(datasource_class.columns), - subqueryload(datasource_class.metrics), - ) - .filter_by(id=datasource_id) - .one() - ) - - @classmethod - def query_datasources_by_name( - cls, - session: Session, - database: Database, - datasource_name: str, - schema: Optional[str] = None, - ) -> List[Datasource]: - datasource_class = DatasourceDAO.sources[DatasourceType[database.type]] - if not isinstance(datasource_class, SqlaTable): - return [] - - return datasource_class.query_datasources_by_name( - session, database, datasource_name, schema=schema - ) diff --git a/superset/dao/exceptions.py b/superset/dao/exceptions.py index 9b5624bd5d31d..93cb25d3fc70e 100644 --- a/superset/dao/exceptions.py +++ b/superset/dao/exceptions.py @@ -60,6 +60,7 @@ class DatasourceTypeNotSupportedError(DAOException): DAO datasource query source type is not supported """ + status = 422 message = "DAO datasource query source type is not supported" diff --git a/superset/dashboards/commands/importers/v0.py b/superset/dashboards/commands/importers/v0.py index 207920b1d2c2a..e49c931896838 100644 --- a/superset/dashboards/commands/importers/v0.py +++ b/superset/dashboards/commands/importers/v0.py @@ -24,7 +24,7 @@ from flask_babel import lazy_gettext as _ from sqlalchemy.orm import make_transient, Session -from superset import ConnectorRegistry, db +from superset import db from superset.commands.base import BaseCommand from superset.connectors.sqla.models import SqlaTable, SqlMetric, TableColumn from superset.datasets.commands.importers.v0 import import_dataset @@ -63,12 +63,11 @@ def import_chart( slc_to_import = slc_to_import.copy() slc_to_import.reset_ownership() params = slc_to_import.params_dict - datasource = ConnectorRegistry.get_datasource_by_name( - session, - slc_to_import.datasource_type, - params["datasource_name"], - params["schema"], - params["database_name"], + datasource = SqlaTable.get_datasource_by_name( + session=session, + datasource_name=params["datasource_name"], + database_name=params["database_name"], + schema=params["schema"], ) slc_to_import.datasource_id = datasource.id # type: ignore if slc_to_override: diff --git a/superset/datasource/__init__.py b/superset/datasource/__init__.py new file mode 100644 index 0000000000000..e0533d99236c2 --- /dev/null +++ b/superset/datasource/__init__.py @@ -0,0 +1,16 @@ +# 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. diff --git a/superset/datasource/dao.py b/superset/datasource/dao.py new file mode 100644 index 0000000000000..c475919abf006 --- /dev/null +++ b/superset/datasource/dao.py @@ -0,0 +1,62 @@ +# 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. + +from typing import Dict, Type, Union + +from sqlalchemy.orm import Session + +from superset.connectors.sqla.models import SqlaTable +from superset.dao.base import BaseDAO +from superset.dao.exceptions import DatasourceNotFound, DatasourceTypeNotSupportedError +from superset.datasets.models import Dataset +from superset.models.sql_lab import Query, SavedQuery +from superset.tables.models import Table +from superset.utils.core import DatasourceType + +Datasource = Union[Dataset, SqlaTable, Table, Query, SavedQuery] + + +class DatasourceDAO(BaseDAO): + + sources: Dict[Union[DatasourceType, str], Type[Datasource]] = { + DatasourceType.TABLE: SqlaTable, + DatasourceType.QUERY: Query, + DatasourceType.SAVEDQUERY: SavedQuery, + DatasourceType.DATASET: Dataset, + DatasourceType.SLTABLE: Table, + } + + @classmethod + def get_datasource( + cls, + session: Session, + datasource_type: Union[DatasourceType, str], + datasource_id: int, + ) -> Datasource: + if datasource_type not in cls.sources: + raise DatasourceTypeNotSupportedError() + + datasource = ( + session.query(cls.sources[datasource_type]) + .filter_by(id=datasource_id) + .one_or_none() + ) + + if not datasource: + raise DatasourceNotFound() + + return datasource diff --git a/superset/examples/helpers.py b/superset/examples/helpers.py index 9d17e73773299..8c2ad29f49102 100644 --- a/superset/examples/helpers.py +++ b/superset/examples/helpers.py @@ -23,7 +23,7 @@ from urllib import request from superset import app, db -from superset.connectors.connector_registry import ConnectorRegistry +from superset.connectors.sqla.models import SqlaTable from superset.models.slice import Slice BASE_URL = "https://github.com/apache-superset/examples-data/blob/master/" @@ -32,7 +32,7 @@ def get_table_connector_registry() -> Any: - return ConnectorRegistry.sources["table"] + return SqlaTable def get_examples_folder() -> str: diff --git a/superset/explore/form_data/commands/create.py b/superset/explore/form_data/commands/create.py index 7946980c82684..5c301a96f1a12 100644 --- a/superset/explore/form_data/commands/create.py +++ b/superset/explore/form_data/commands/create.py @@ -27,6 +27,7 @@ from superset.key_value.utils import get_owner, random_key from superset.temporary_cache.commands.exceptions import TemporaryCacheCreateFailedError from superset.temporary_cache.utils import cache_key +from superset.utils.core import DatasourceType from superset.utils.schema import validate_json logger = logging.getLogger(__name__) @@ -56,7 +57,7 @@ def run(self) -> str: state: TemporaryExploreState = { "owner": get_owner(actor), "datasource_id": datasource_id, - "datasource_type": datasource_type, + "datasource_type": DatasourceType(datasource_type), "chart_id": chart_id, "form_data": form_data, } diff --git a/superset/explore/form_data/commands/state.py b/superset/explore/form_data/commands/state.py index 470f2e22f5989..35e3893478ea0 100644 --- a/superset/explore/form_data/commands/state.py +++ b/superset/explore/form_data/commands/state.py @@ -18,10 +18,12 @@ from typing_extensions import TypedDict +from superset.utils.core import DatasourceType + class TemporaryExploreState(TypedDict): owner: Optional[int] datasource_id: int - datasource_type: str + datasource_type: DatasourceType chart_id: Optional[int] form_data: str diff --git a/superset/explore/form_data/commands/update.py b/superset/explore/form_data/commands/update.py index fdc75093bef85..f48d8e85ef5ba 100644 --- a/superset/explore/form_data/commands/update.py +++ b/superset/explore/form_data/commands/update.py @@ -32,6 +32,7 @@ TemporaryCacheUpdateFailedError, ) from superset.temporary_cache.utils import cache_key +from superset.utils.core import DatasourceType from superset.utils.schema import validate_json logger = logging.getLogger(__name__) @@ -75,7 +76,7 @@ def run(self) -> Optional[str]: new_state: TemporaryExploreState = { "owner": owner, "datasource_id": datasource_id, - "datasource_type": datasource_type, + "datasource_type": DatasourceType(datasource_type), "chart_id": chart_id, "form_data": form_data, } diff --git a/superset/initialization/__init__.py b/superset/initialization/__init__.py index 698c3881390ef..426dc1b524d19 100644 --- a/superset/initialization/__init__.py +++ b/superset/initialization/__init__.py @@ -28,7 +28,6 @@ from flask_compress import Compress from werkzeug.middleware.proxy_fix import ProxyFix -from superset.connectors.connector_registry import ConnectorRegistry from superset.constants import CHANGE_ME_SECRET_KEY from superset.extensions import ( _event_logger, @@ -473,7 +472,11 @@ def configure_data_sources(self) -> None: # Registering sources module_datasource_map = self.config["DEFAULT_MODULE_DS_MAP"] module_datasource_map.update(self.config["ADDITIONAL_MODULE_DS_MAP"]) - ConnectorRegistry.register_sources(module_datasource_map) + + # todo(hughhhh): fully remove the datasource config register + for module_name, class_names in module_datasource_map.items(): + class_names = [str(s) for s in class_names] + __import__(module_name, fromlist=class_names) def configure_cache(self) -> None: cache_manager.init_app(self.superset_app) diff --git a/superset/models/dashboard.py b/superset/models/dashboard.py index f2d53e1ff5e6a..12f7056161328 100644 --- a/superset/models/dashboard.py +++ b/superset/models/dashboard.py @@ -46,10 +46,11 @@ from sqlalchemy.sql import join, select from sqlalchemy.sql.elements import BinaryExpression -from superset import app, ConnectorRegistry, db, is_feature_enabled, security_manager +from superset import app, db, is_feature_enabled, security_manager from superset.common.request_contexed_based import is_user_admin from superset.connectors.base.models import BaseDatasource -from superset.connectors.sqla.models import SqlMetric, TableColumn +from superset.connectors.sqla.models import SqlaTable, SqlMetric, TableColumn +from superset.datasource.dao import DatasourceDAO from superset.extensions import cache_manager from superset.models.filter_set import FilterSet from superset.models.helpers import AuditMixinNullable, ImportExportMixin @@ -407,16 +408,18 @@ def export_dashboards( # pylint: disable=too-many-locals id_ = target.get("datasetId") if id_ is None: continue - datasource = ConnectorRegistry.get_datasource_by_id(session, id_) + datasource = DatasourceDAO.get_datasource( + session, utils.DatasourceType.TABLE, id_ + ) datasource_ids.add((datasource.id, datasource.type)) copied_dashboard.alter_params(remote_id=dashboard_id) copied_dashboards.append(copied_dashboard) eager_datasources = [] - for datasource_id, datasource_type in datasource_ids: - eager_datasource = ConnectorRegistry.get_eager_datasource( - db.session, datasource_type, datasource_id + for datasource_id, _ in datasource_ids: + eager_datasource = SqlaTable.get_eager_sqlatable_datasource( + db.session, datasource_id ) copied_datasource = eager_datasource.copy() copied_datasource.alter_params( diff --git a/superset/models/datasource_access_request.py b/superset/models/datasource_access_request.py index fa3b9d67113d3..60bfe08238284 100644 --- a/superset/models/datasource_access_request.py +++ b/superset/models/datasource_access_request.py @@ -21,7 +21,6 @@ from sqlalchemy import Column, Integer, String from superset import app, db, security_manager -from superset.connectors.connector_registry import ConnectorRegistry from superset.models.helpers import AuditMixinNullable from superset.utils.memoized import memoized @@ -44,7 +43,10 @@ class DatasourceAccessRequest(Model, AuditMixinNullable): @property def cls_model(self) -> Type["BaseDatasource"]: - return ConnectorRegistry.sources[self.datasource_type] + # pylint: disable=import-outside-toplevel + from superset.datasource.dao import DatasourceDAO + + return DatasourceDAO.sources[self.datasource_type] @property def username(self) -> Markup: diff --git a/superset/models/slice.py b/superset/models/slice.py index 862edb9ec8ce8..841539bc66573 100644 --- a/superset/models/slice.py +++ b/superset/models/slice.py @@ -39,7 +39,7 @@ from sqlalchemy.orm import relationship from sqlalchemy.orm.mapper import Mapper -from superset import ConnectorRegistry, db, is_feature_enabled, security_manager +from superset import db, is_feature_enabled, security_manager from superset.legacy import update_time_range from superset.models.helpers import AuditMixinNullable, ImportExportMixin from superset.models.tags import ChartUpdater @@ -126,7 +126,10 @@ def __repr__(self) -> str: @property def cls_model(self) -> Type["BaseDatasource"]: - return ConnectorRegistry.sources[self.datasource_type] + # pylint: disable=import-outside-toplevel + from superset.datasource.dao import DatasourceDAO + + return DatasourceDAO.sources[self.datasource_type] @property def datasource(self) -> Optional["BaseDatasource"]: diff --git a/superset/security/manager.py b/superset/security/manager.py index 6157959aa3739..890a09415ecb3 100644 --- a/superset/security/manager.py +++ b/superset/security/manager.py @@ -61,7 +61,6 @@ from sqlalchemy.orm.query import Query as SqlaQuery from superset import sql_parse -from superset.connectors.connector_registry import ConnectorRegistry from superset.constants import RouteMethod from superset.errors import ErrorLevel, SupersetError, SupersetErrorType from superset.exceptions import ( @@ -471,23 +470,25 @@ def get_user_datasources(self) -> List["BaseDatasource"]: user_perms = self.user_view_menu_names("datasource_access") schema_perms = self.user_view_menu_names("schema_access") user_datasources = set() - for datasource_class in ConnectorRegistry.sources.values(): - user_datasources.update( - self.get_session.query(datasource_class) - .filter( - or_( - datasource_class.perm.in_(user_perms), - datasource_class.schema_perm.in_(schema_perms), - ) + + # pylint: disable=import-outside-toplevel + from superset.connectors.sqla.models import SqlaTable + + user_datasources.update( + self.get_session.query(SqlaTable) + .filter( + or_( + SqlaTable.perm.in_(user_perms), + SqlaTable.schema_perm.in_(schema_perms), ) - .all() ) + .all() + ) # group all datasources by database - all_datasources = ConnectorRegistry.get_all_datasources(self.get_session) - datasources_by_database: Dict["Database", Set["BaseDatasource"]] = defaultdict( - set - ) + session = self.get_session + all_datasources = SqlaTable.get_all_datasources(session) + datasources_by_database: Dict["Database", Set["SqlaTable"]] = defaultdict(set) for datasource in all_datasources: datasources_by_database[datasource.database].add(datasource) @@ -599,6 +600,8 @@ def get_datasources_accessible_by_user( # pylint: disable=invalid-name :param schema: The fallback SQL schema if not present in the table name :returns: The list of accessible SQL tables w/ schema """ + # pylint: disable=import-outside-toplevel + from superset.connectors.sqla.models import SqlaTable if self.can_access_database(database): return datasource_names @@ -610,7 +613,7 @@ def get_datasources_accessible_by_user( # pylint: disable=invalid-name user_perms = self.user_view_menu_names("datasource_access") schema_perms = self.user_view_menu_names("schema_access") - user_datasources = ConnectorRegistry.query_datasources_by_permissions( + user_datasources = SqlaTable.query_datasources_by_permissions( self.get_session, database, user_perms, schema_perms ) if schema: @@ -660,6 +663,7 @@ def create_missing_perms(self) -> None: """ # pylint: disable=import-outside-toplevel + from superset.connectors.sqla.models import SqlaTable from superset.models import core as models logger.info("Fetching a set of all perms to lookup which ones are missing") @@ -668,13 +672,13 @@ def create_missing_perms(self) -> None: if pv.permission and pv.view_menu: all_pvs.add((pv.permission.name, pv.view_menu.name)) - def merge_pv(view_menu: str, perm: str) -> None: + def merge_pv(view_menu: str, perm: Optional[str]) -> None: """Create permission view menu only if it doesn't exist""" if view_menu and perm and (view_menu, perm) not in all_pvs: self.add_permission_view_menu(view_menu, perm) logger.info("Creating missing datasource permissions.") - datasources = ConnectorRegistry.get_all_datasources(self.get_session) + datasources = SqlaTable.get_all_datasources(self.get_session) for datasource in datasources: merge_pv("datasource_access", datasource.get_perm()) merge_pv("schema_access", datasource.get_schema_perm()) diff --git a/superset/views/core.py b/superset/views/core.py index f65385fc305a5..04d3835f60322 100755 --- a/superset/views/core.py +++ b/superset/views/core.py @@ -61,7 +61,6 @@ from superset.common.chart_data import ChartDataResultFormat, ChartDataResultType from superset.common.db_query_status import QueryStatus from superset.connectors.base.models import BaseDatasource -from superset.connectors.connector_registry import ConnectorRegistry from superset.connectors.sqla.models import ( AnnotationDatasource, SqlaTable, @@ -77,6 +76,7 @@ from superset.databases.filters import DatabaseFilter from superset.databases.utils import make_url_safe from superset.datasets.commands.exceptions import DatasetNotFoundError +from superset.datasource.dao import DatasourceDAO from superset.errors import ErrorLevel, SupersetError, SupersetErrorType from superset.exceptions import ( CacheLoadError, @@ -129,7 +129,11 @@ from superset.utils import core as utils, csv from superset.utils.async_query_manager import AsyncQueryTokenException from superset.utils.cache import etag_cache -from superset.utils.core import apply_max_row_limit, ReservedUrlParameters +from superset.utils.core import ( + apply_max_row_limit, + DatasourceType, + ReservedUrlParameters, +) from superset.utils.dates import now_as_float from superset.utils.decorators import check_dashboard_access from superset.views.base import ( @@ -250,7 +254,7 @@ def override_role_permissions(self) -> FlaskResponse: ) db_ds_names.add(fullname) - existing_datasources = ConnectorRegistry.get_all_datasources(db.session) + existing_datasources = SqlaTable.get_all_datasources(db.session) datasources = [d for d in existing_datasources if d.full_name in db_ds_names] role = security_manager.find_role(role_name) # remove all permissions @@ -282,7 +286,7 @@ def request_access(self) -> FlaskResponse: datasource_id = request.args.get("datasource_id") datasource_type = request.args.get("datasource_type") if datasource_id and datasource_type: - ds_class = ConnectorRegistry.sources.get(datasource_type) + ds_class = DatasourceDAO.sources.get(datasource_type) datasource = ( db.session.query(ds_class).filter_by(id=int(datasource_id)).one() ) @@ -319,10 +323,8 @@ def request_access(self) -> FlaskResponse: def approve(self) -> FlaskResponse: # pylint: disable=too-many-locals,no-self-use def clean_fulfilled_requests(session: Session) -> None: for dar in session.query(DAR).all(): - datasource = ConnectorRegistry.get_datasource( - dar.datasource_type, - dar.datasource_id, - session, + datasource = DatasourceDAO.get_datasource( + session, DatasourceType(dar.datasource_type), dar.datasource_id ) if not datasource or security_manager.can_access_datasource(datasource): # Dataset does not exist anymore @@ -336,8 +338,8 @@ def clean_fulfilled_requests(session: Session) -> None: role_to_extend = request.args.get("role_to_extend") session = db.session - datasource = ConnectorRegistry.get_datasource( - datasource_type, datasource_id, session + datasource = DatasourceDAO.get_datasource( + session, DatasourceType(datasource_type), int(datasource_id) ) if not datasource: @@ -639,7 +641,6 @@ def explore_json( datasource_id, datasource_type = get_datasource_info( datasource_id, datasource_type, form_data ) - force = request.args.get("force") == "true" # TODO: support CSV, SQL query and other non-JSON types @@ -809,8 +810,10 @@ def explore( datasource: Optional[BaseDatasource] = None if datasource_id is not None: try: - datasource = ConnectorRegistry.get_datasource( - cast(str, datasource_type), datasource_id, db.session + datasource = DatasourceDAO.get_datasource( + db.session, + DatasourceType(cast(str, datasource_type)), + datasource_id, ) except DatasetNotFoundError: pass @@ -948,10 +951,8 @@ def filter( # pylint: disable=no-self-use :raises SupersetSecurityException: If the user cannot access the resource """ # TODO: Cache endpoint by user, datasource and column - datasource = ConnectorRegistry.get_datasource( - datasource_type, - datasource_id, - db.session, + datasource = DatasourceDAO.get_datasource( + db.session, DatasourceType(datasource_type), datasource_id ) if not datasource: return json_error_response(DATASOURCE_MISSING_ERR) @@ -1920,8 +1921,8 @@ def dashboard( if config["ENABLE_ACCESS_REQUEST"]: for datasource in dashboard.datasources: - datasource = ConnectorRegistry.get_datasource( - datasource_type=datasource.type, + datasource = DatasourceDAO.get_datasource( + datasource_type=DatasourceType(datasource.type), datasource_id=datasource.id, session=db.session(), ) @@ -2537,10 +2538,8 @@ def fetch_datasource_metadata(self) -> FlaskResponse: # pylint: disable=no-self """ datasource_id, datasource_type = request.args["datasourceKey"].split("__") - datasource = ConnectorRegistry.get_datasource( - datasource_type, - datasource_id, - db.session, + datasource = DatasourceDAO.get_datasource( + db.session, DatasourceType(datasource_type), int(datasource_id) ) # Check if datasource exists if not datasource: diff --git a/superset/views/datasource/views.py b/superset/views/datasource/views.py index 560c12d6f19b5..bf67eddd01199 100644 --- a/superset/views/datasource/views.py +++ b/superset/views/datasource/views.py @@ -29,16 +29,18 @@ from superset import db, event_logger from superset.commands.utils import populate_owners -from superset.connectors.connector_registry import ConnectorRegistry +from superset.connectors.sqla.models import SqlaTable from superset.connectors.sqla.utils import get_physical_table_metadata from superset.datasets.commands.exceptions import ( DatasetForbiddenError, DatasetNotFoundError, ) +from superset.datasource.dao import DatasourceDAO from superset.exceptions import SupersetException, SupersetSecurityException from superset.extensions import security_manager from superset.models.core import Database from superset.superset_typing import FlaskResponse +from superset.utils.core import DatasourceType from superset.views.base import ( api, BaseSupersetView, @@ -74,8 +76,8 @@ def save(self) -> FlaskResponse: datasource_id = datasource_dict.get("id") datasource_type = datasource_dict.get("type") database_id = datasource_dict["database"].get("id") - orm_datasource = ConnectorRegistry.get_datasource( - datasource_type, datasource_id, db.session + orm_datasource = DatasourceDAO.get_datasource( + db.session, DatasourceType(datasource_type), datasource_id ) orm_datasource.database_id = database_id @@ -117,8 +119,8 @@ def save(self) -> FlaskResponse: @api @handle_api_exception def get(self, datasource_type: str, datasource_id: int) -> FlaskResponse: - datasource = ConnectorRegistry.get_datasource( - datasource_type, datasource_id, db.session + datasource = DatasourceDAO.get_datasource( + db.session, DatasourceType(datasource_type), datasource_id ) return self.json_response(sanitize_datasource_data(datasource.data)) @@ -130,8 +132,10 @@ def external_metadata( self, datasource_type: str, datasource_id: int ) -> FlaskResponse: """Gets column info from the source system""" - datasource = ConnectorRegistry.get_datasource( - datasource_type, datasource_id, db.session + datasource = DatasourceDAO.get_datasource( + db.session, + DatasourceType(datasource_type), + datasource_id, ) try: external_metadata = datasource.external_metadata() @@ -153,9 +157,8 @@ def external_metadata_by_name(self, **kwargs: Any) -> FlaskResponse: except ValidationError as err: return json_error_response(str(err), status=400) - datasource = ConnectorRegistry.get_datasource_by_name( + datasource = SqlaTable.get_datasource_by_name( session=db.session, - datasource_type=params["datasource_type"], database_name=params["database_name"], schema=params["schema_name"], datasource_name=params["table_name"], diff --git a/superset/views/utils.py b/superset/views/utils.py index e0f97cba1839b..719642ef13a96 100644 --- a/superset/views/utils.py +++ b/superset/views/utils.py @@ -32,7 +32,7 @@ import superset.models.core as models from superset import app, dataframe, db, result_set, viz from superset.common.db_query_status import QueryStatus -from superset.connectors.connector_registry import ConnectorRegistry +from superset.datasource.dao import DatasourceDAO from superset.errors import ErrorLevel, SupersetError, SupersetErrorType from superset.exceptions import ( CacheLoadError, @@ -47,6 +47,7 @@ from superset.models.slice import Slice from superset.models.sql_lab import Query from superset.superset_typing import FormData +from superset.utils.core import DatasourceType from superset.utils.decorators import stats_timing from superset.viz import BaseViz @@ -127,8 +128,10 @@ def get_viz( force_cached: bool = False, ) -> BaseViz: viz_type = form_data.get("viz_type", "table") - datasource = ConnectorRegistry.get_datasource( - datasource_type, datasource_id, db.session + datasource = DatasourceDAO.get_datasource( + db.session, + DatasourceType(datasource_type), + datasource_id, ) viz_obj = viz.viz_types[viz_type]( datasource, form_data=form_data, force=force, force_cached=force_cached diff --git a/tests/integration_tests/access_tests.py b/tests/integration_tests/access_tests.py index c2319ff5b52c1..59b5b19b298c4 100644 --- a/tests/integration_tests/access_tests.py +++ b/tests/integration_tests/access_tests.py @@ -39,7 +39,6 @@ ) from tests.integration_tests.test_app import app # isort:skip from superset import db, security_manager -from superset.connectors.connector_registry import ConnectorRegistry from superset.connectors.sqla.models import SqlaTable from superset.models import core as models from superset.models.datasource_access_request import DatasourceAccessRequest @@ -90,12 +89,12 @@ def create_access_request(session, ds_type, ds_name, role_name, username): - ds_class = ConnectorRegistry.sources[ds_type] # TODO: generalize datasource names if ds_type == "table": - ds = session.query(ds_class).filter(ds_class.table_name == ds_name).first() + ds = session.query(SqlaTable).filter(SqlaTable.table_name == ds_name).first() else: - ds = session.query(ds_class).filter(ds_class.datasource_name == ds_name).first() + # This function will only work for ds_type == "table" + raise NotImplementedError() ds_perm_view = security_manager.find_permission_view_menu( "datasource_access", ds.perm ) @@ -449,49 +448,6 @@ def test_approve(self, mock_send_mime): TEST_ROLE = security_manager.find_role(TEST_ROLE_NAME) self.assertIn(perm_view, TEST_ROLE.permissions) - # Case 3. Grant new role to the user to access the druid datasource. - - security_manager.add_role("druid_role") - access_request3 = create_access_request( - session, "druid", "druid_ds_1", "druid_role", "gamma" - ) - self.get_resp( - GRANT_ROLE_REQUEST.format( - "druid", access_request3.datasource_id, "gamma", "druid_role" - ) - ) - - # user was granted table_role - user_roles = [r.name for r in security_manager.find_user("gamma").roles] - self.assertIn("druid_role", user_roles) - - # Case 4. Extend the role to have access to the druid datasource - - access_request4 = create_access_request( - session, "druid", "druid_ds_2", "druid_role", "gamma" - ) - druid_ds_2_perm = access_request4.datasource.perm - - self.client.get( - EXTEND_ROLE_REQUEST.format( - "druid", access_request4.datasource_id, "gamma", "druid_role" - ) - ) - # druid_role was extended to grant access to the druid_access_ds_2 - druid_role = security_manager.find_role("druid_role") - perm_view = security_manager.find_permission_view_menu( - "datasource_access", druid_ds_2_perm - ) - self.assertIn(perm_view, druid_role.permissions) - - # cleanup - gamma_user = security_manager.find_user(username="gamma") - gamma_user.roles.remove(security_manager.find_role("druid_role")) - gamma_user.roles.remove(security_manager.find_role(TEST_ROLE_NAME)) - session.delete(security_manager.find_role("druid_role")) - session.delete(security_manager.find_role(TEST_ROLE_NAME)) - session.commit() - def test_request_access(self): if app.config["ENABLE_ACCESS_REQUEST"]: session = db.session diff --git a/tests/integration_tests/dashboard_utils.py b/tests/integration_tests/dashboard_utils.py index 41a34fa36edf5..115d3269f2e50 100644 --- a/tests/integration_tests/dashboard_utils.py +++ b/tests/integration_tests/dashboard_utils.py @@ -21,7 +21,7 @@ from pandas import DataFrame -from superset import ConnectorRegistry, db +from superset import db from superset.connectors.sqla.models import SqlaTable from superset.models.core import Database from superset.models.dashboard import Dashboard @@ -35,9 +35,8 @@ def get_table( schema: Optional[str] = None, ): schema = schema or get_example_default_schema() - table_source = ConnectorRegistry.sources["table"] return ( - db.session.query(table_source) + db.session.query(SqlaTable) .filter_by(database_id=database.id, schema=schema, table_name=table_name) .one_or_none() ) @@ -54,8 +53,7 @@ def create_table_metadata( table = get_table(table_name, database, schema) if not table: - table_source = ConnectorRegistry.sources["table"] - table = table_source(schema=schema, table_name=table_name) + table = SqlaTable(schema=schema, table_name=table_name) if fetch_values_predicate: table.fetch_values_predicate = fetch_values_predicate table.database = database diff --git a/tests/integration_tests/datasource_tests.py b/tests/integration_tests/datasource_tests.py index 6d46afa0a9ddd..6c8ae672c5845 100644 --- a/tests/integration_tests/datasource_tests.py +++ b/tests/integration_tests/datasource_tests.py @@ -22,12 +22,13 @@ import prison import pytest -from superset import app, ConnectorRegistry, db +from superset import app, db from superset.connectors.sqla.models import SqlaTable +from superset.dao.exceptions import DatasourceNotFound, DatasourceTypeNotSupportedError from superset.datasets.commands.exceptions import DatasetNotFoundError from superset.exceptions import SupersetGenericDBErrorException from superset.models.core import Database -from superset.utils.core import get_example_default_schema +from superset.utils.core import DatasourceType, get_example_default_schema from superset.utils.database import get_example_database from tests.integration_tests.base_tests import db_insert_temp_object, SupersetTestCase from tests.integration_tests.fixtures.birth_names_dashboard import ( @@ -256,9 +257,10 @@ def test_external_metadata_error_return_400(self, mock_get_datasource): pytest.raises( SupersetGenericDBErrorException, - lambda: ConnectorRegistry.get_datasource( - "table", tbl.id, db.session - ).external_metadata(), + lambda: db.session.query(SqlaTable) + .filter_by(id=tbl.id) + .one_or_none() + .external_metadata(), ) resp = self.client.get(url) @@ -385,21 +387,30 @@ def my_check(datasource): app.config["DATASET_HEALTH_CHECK"] = my_check self.login(username="admin") tbl = self.get_table(name="birth_names") - datasource = ConnectorRegistry.get_datasource("table", tbl.id, db.session) + datasource = db.session.query(SqlaTable).filter_by(id=tbl.id).one_or_none() assert datasource.health_check_message == "Warning message!" app.config["DATASET_HEALTH_CHECK"] = None def test_get_datasource_failed(self): + from superset.datasource.dao import DatasourceDAO + pytest.raises( - DatasetNotFoundError, - lambda: ConnectorRegistry.get_datasource("table", 9999999, db.session), + DatasourceNotFound, + lambda: DatasourceDAO.get_datasource(db.session, "table", 9999999), ) self.login(username="admin") - resp = self.get_json_resp("/datasource/get/druid/500000/", raise_on_error=False) - self.assertEqual(resp.get("error"), "Dataset does not exist") + resp = self.get_json_resp("/datasource/get/table/500000/", raise_on_error=False) + self.assertEqual(resp.get("error"), "Datasource does not exist") - resp = self.get_json_resp( - "/datasource/get/invalid-datasource-type/500000/", raise_on_error=False + def test_get_datasource_invalid_datasource_failed(self): + from superset.datasource.dao import DatasourceDAO + + pytest.raises( + DatasourceTypeNotSupportedError, + lambda: DatasourceDAO.get_datasource(db.session, "druid", 9999999), ) - self.assertEqual(resp.get("error"), "Dataset does not exist") + + self.login(username="admin") + resp = self.get_json_resp("/datasource/get/druid/500000/", raise_on_error=False) + self.assertEqual(resp.get("error"), "'druid' is not a valid DatasourceType") diff --git a/tests/integration_tests/explore/form_data/api_tests.py b/tests/integration_tests/explore/form_data/api_tests.py index 8b375df56ae38..dae713ff7041b 100644 --- a/tests/integration_tests/explore/form_data/api_tests.py +++ b/tests/integration_tests/explore/form_data/api_tests.py @@ -26,6 +26,7 @@ from superset.explore.form_data.commands.state import TemporaryExploreState from superset.extensions import cache_manager from superset.models.slice import Slice +from superset.utils.core import DatasourceType from tests.integration_tests.base_tests import login from tests.integration_tests.fixtures.client import client from tests.integration_tests.fixtures.world_bank_dashboard import ( @@ -392,7 +393,7 @@ def test_delete_not_owner(client, chart_id: int, datasource: SqlaTable, admin_id entry: TemporaryExploreState = { "owner": another_owner, "datasource_id": datasource.id, - "datasource_type": datasource.type, + "datasource_type": DatasourceType(datasource.type), "chart_id": chart_id, "form_data": INITIAL_FORM_DATA, } diff --git a/tests/integration_tests/fixtures/birth_names_dashboard.py b/tests/integration_tests/fixtures/birth_names_dashboard.py index ef71803aa5db7..0434e22295267 100644 --- a/tests/integration_tests/fixtures/birth_names_dashboard.py +++ b/tests/integration_tests/fixtures/birth_names_dashboard.py @@ -18,7 +18,7 @@ import pytest -from superset import ConnectorRegistry, db +from superset import db from superset.connectors.sqla.models import SqlaTable from superset.models.core import Database from superset.models.dashboard import Dashboard @@ -95,14 +95,11 @@ def _create_table( def _cleanup(dash_id: int, slices_ids: List[int]) -> None: schema = get_example_default_schema() - - table_id = ( + datasource = ( db.session.query(SqlaTable) .filter_by(table_name="birth_names", schema=schema) .one() - .id ) - datasource = ConnectorRegistry.get_datasource("table", table_id, db.session) columns = [column for column in datasource.columns] metrics = [metric for metric in datasource.metrics] diff --git a/tests/integration_tests/fixtures/energy_dashboard.py b/tests/integration_tests/fixtures/energy_dashboard.py index c0291db2a9864..0279fe8ff2f5c 100644 --- a/tests/integration_tests/fixtures/energy_dashboard.py +++ b/tests/integration_tests/fixtures/energy_dashboard.py @@ -82,7 +82,6 @@ def _create_energy_table(): table.metrics.append( SqlMetric(metric_name="sum__value", expression=f"SUM({col})") ) - db.session.merge(table) db.session.commit() table.fetch_metadata() diff --git a/tests/integration_tests/insert_chart_mixin.py b/tests/integration_tests/insert_chart_mixin.py index 8fcb33067e351..da05d0c49d043 100644 --- a/tests/integration_tests/insert_chart_mixin.py +++ b/tests/integration_tests/insert_chart_mixin.py @@ -16,7 +16,8 @@ # under the License. from typing import List, Optional -from superset import ConnectorRegistry, db, security_manager +from superset import db, security_manager +from superset.connectors.sqla.models import SqlaTable from superset.models.slice import Slice @@ -43,8 +44,8 @@ def insert_chart( for owner in owners: user = db.session.query(security_manager.user_model).get(owner) obj_owners.append(user) - datasource = ConnectorRegistry.get_datasource( - datasource_type, datasource_id, db.session + datasource = ( + db.session.query(SqlaTable).filter_by(id=datasource_id).one_or_none() ) slice = Slice( cache_timeout=cache_timeout, diff --git a/tests/integration_tests/query_context_tests.py b/tests/integration_tests/query_context_tests.py index 816267678f9e0..6d5fec88f444d 100644 --- a/tests/integration_tests/query_context_tests.py +++ b/tests/integration_tests/query_context_tests.py @@ -26,10 +26,15 @@ from superset.common.chart_data import ChartDataResultFormat, ChartDataResultType from superset.common.query_context import QueryContext from superset.common.query_object import QueryObject -from superset.connectors.connector_registry import ConnectorRegistry from superset.connectors.sqla.models import SqlMetric +from superset.datasource.dao import DatasourceDAO from superset.extensions import cache_manager -from superset.utils.core import AdhocMetricExpressionType, backend, QueryStatus +from superset.utils.core import ( + AdhocMetricExpressionType, + backend, + DatasourceType, + QueryStatus, +) from tests.integration_tests.base_tests import SupersetTestCase from tests.integration_tests.fixtures.birth_names_dashboard import ( load_birth_names_dashboard_with_slices, @@ -132,10 +137,10 @@ def test_query_cache_key_changes_when_datasource_is_updated(self): cache_key_original = query_context.query_cache_key(query_object) # make temporary change and revert it to refresh the changed_on property - datasource = ConnectorRegistry.get_datasource( - datasource_type=payload["datasource"]["type"], - datasource_id=payload["datasource"]["id"], + datasource = DatasourceDAO.get_datasource( session=db.session, + datasource_type=DatasourceType(payload["datasource"]["type"]), + datasource_id=payload["datasource"]["id"], ) description_original = datasource.description datasource.description = "temporary description" @@ -156,10 +161,10 @@ def test_query_cache_key_changes_when_metric_is_updated(self): payload = get_query_context("birth_names") # make temporary change and revert it to refresh the changed_on property - datasource = ConnectorRegistry.get_datasource( - datasource_type=payload["datasource"]["type"], - datasource_id=payload["datasource"]["id"], + datasource = DatasourceDAO.get_datasource( session=db.session, + datasource_type=DatasourceType(payload["datasource"]["type"]), + datasource_id=payload["datasource"]["id"], ) datasource.metrics.append(SqlMetric(metric_name="foo", expression="select 1;")) diff --git a/tests/integration_tests/security_tests.py b/tests/integration_tests/security_tests.py index a70146db68321..045e368296e85 100644 --- a/tests/integration_tests/security_tests.py +++ b/tests/integration_tests/security_tests.py @@ -28,10 +28,11 @@ import pytest from flask import current_app +from superset.datasource.dao import DatasourceDAO from superset.models.dashboard import Dashboard -from superset import app, appbuilder, db, security_manager, viz, ConnectorRegistry +from superset import app, appbuilder, db, security_manager, viz from superset.connectors.sqla.models import SqlaTable from superset.errors import ErrorLevel, SupersetError, SupersetErrorType from superset.exceptions import SupersetSecurityException @@ -990,7 +991,7 @@ def test_get_user_datasources_admin( mock_get_session.query.return_value.filter.return_value.all.return_value = [] with mock.patch.object( - ConnectorRegistry, "get_all_datasources" + SqlaTable, "get_all_datasources" ) as mock_get_all_datasources: mock_get_all_datasources.return_value = [ Datasource("database1", "schema1", "table1"), @@ -1018,7 +1019,7 @@ def test_get_user_datasources_gamma( mock_get_session.query.return_value.filter.return_value.all.return_value = [] with mock.patch.object( - ConnectorRegistry, "get_all_datasources" + SqlaTable, "get_all_datasources" ) as mock_get_all_datasources: mock_get_all_datasources.return_value = [ Datasource("database1", "schema1", "table1"), @@ -1046,7 +1047,7 @@ def test_get_user_datasources_gamma_with_schema( ] with mock.patch.object( - ConnectorRegistry, "get_all_datasources" + SqlaTable, "get_all_datasources" ) as mock_get_all_datasources: mock_get_all_datasources.return_value = [ Datasource("database1", "schema1", "table1"), diff --git a/tests/unit_tests/dao/datasource_test.py b/tests/unit_tests/datasource/dao_tests.py similarity index 81% rename from tests/unit_tests/dao/datasource_test.py rename to tests/unit_tests/datasource/dao_tests.py index a15684d71e699..0682c19c28756 100644 --- a/tests/unit_tests/dao/datasource_test.py +++ b/tests/unit_tests/datasource/dao_tests.py @@ -103,7 +103,7 @@ def test_get_datasource_sqlatable( app_context: None, session_with_data: Session ) -> None: from superset.connectors.sqla.models import SqlaTable - from superset.dao.datasource.dao import DatasourceDAO + from superset.datasource.dao import DatasourceDAO result = DatasourceDAO.get_datasource( datasource_type=DatasourceType.TABLE, @@ -117,7 +117,7 @@ def test_get_datasource_sqlatable( def test_get_datasource_query(app_context: None, session_with_data: Session) -> None: - from superset.dao.datasource.dao import DatasourceDAO + from superset.datasource.dao import DatasourceDAO from superset.models.sql_lab import Query result = DatasourceDAO.get_datasource( @@ -131,7 +131,7 @@ def test_get_datasource_query(app_context: None, session_with_data: Session) -> def test_get_datasource_saved_query( app_context: None, session_with_data: Session ) -> None: - from superset.dao.datasource.dao import DatasourceDAO + from superset.datasource.dao import DatasourceDAO from superset.models.sql_lab import SavedQuery result = DatasourceDAO.get_datasource( @@ -145,7 +145,7 @@ def test_get_datasource_saved_query( def test_get_datasource_sl_table(app_context: None, session_with_data: Session) -> None: - from superset.dao.datasource.dao import DatasourceDAO + from superset.datasource.dao import DatasourceDAO from superset.tables.models import Table # todo(hugh): This will break once we remove the dual write @@ -163,8 +163,8 @@ def test_get_datasource_sl_table(app_context: None, session_with_data: Session) def test_get_datasource_sl_dataset( app_context: None, session_with_data: Session ) -> None: - from superset.dao.datasource.dao import DatasourceDAO from superset.datasets.models import Dataset + from superset.datasource.dao import DatasourceDAO # todo(hugh): This will break once we remove the dual write # update the datsource_id=1 and this will pass again @@ -178,10 +178,35 @@ def test_get_datasource_sl_dataset( assert isinstance(result, Dataset) -def test_get_all_sqlatables_datasources( +def test_get_datasource_w_str_param( app_context: None, session_with_data: Session ) -> None: - from superset.dao.datasource.dao import DatasourceDAO + from superset.connectors.sqla.models import SqlaTable + from superset.datasets.models import Dataset + from superset.datasource.dao import DatasourceDAO + from superset.tables.models import Table + + assert isinstance( + DatasourceDAO.get_datasource( + datasource_type="table", + datasource_id=1, + session=session_with_data, + ), + SqlaTable, + ) + + assert isinstance( + DatasourceDAO.get_datasource( + datasource_type="sl_table", + datasource_id=1, + session=session_with_data, + ), + Table, + ) + + +def test_get_all_datasources(app_context: None, session_with_data: Session) -> None: + from superset.connectors.sqla.models import SqlaTable - result = DatasourceDAO.get_all_sqlatables_datasources(session=session_with_data) + result = SqlaTable.get_all_datasources(session=session_with_data) assert len(result) == 1 From ba4ba0267e6f9f6ce6fed09c5d89965eee5c9f92 Mon Sep 17 00:00:00 2001 From: Daniel Vaz Gaspar Date: Tue, 21 Jun 2022 13:07:23 +0100 Subject: [PATCH 62/71] fix: RLS new db migration downgrade fails on SQLite (#20449) --- ...-06-19_16-17_f3afaf1f11f0_add_unique_name_desc_rls.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/superset/migrations/versions/2022-06-19_16-17_f3afaf1f11f0_add_unique_name_desc_rls.py b/superset/migrations/versions/2022-06-19_16-17_f3afaf1f11f0_add_unique_name_desc_rls.py index 0d8b3334a4fcf..3a6cc03be0ba7 100644 --- a/superset/migrations/versions/2022-06-19_16-17_f3afaf1f11f0_add_unique_name_desc_rls.py +++ b/superset/migrations/versions/2022-06-19_16-17_f3afaf1f11f0_add_unique_name_desc_rls.py @@ -72,8 +72,7 @@ def upgrade(): def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint("uq_rls_name", "row_level_security_filters", type_="unique") - op.drop_column("row_level_security_filters", "description") - op.drop_column("row_level_security_filters", "name") - # ### end Alembic commands ### + with op.batch_alter_table("row_level_security_filters") as batch_op: + batch_op.drop_constraint("uq_rls_name", type_="unique") + batch_op.drop_column("description") + batch_op.drop_column("name") From 93774d1860fd40dfee1f18e2787d9d0b79b551e2 Mon Sep 17 00:00:00 2001 From: Diego Medina Date: Tue, 21 Jun 2022 09:10:37 -0300 Subject: [PATCH 63/71] fix: table viz sort icon bottom aligned (#20447) --- superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx index f0b125940bb83..3b037cd0c15d5 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx +++ b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx @@ -449,7 +449,7 @@ export default function TableChart( data-column-name={col.id} css={{ display: 'inline-flex', - alignItems: 'center', + alignItems: 'flex-end', }} > {label} From 8bbbd6f03fbd7fccf457706d942f114b7abb682d Mon Sep 17 00:00:00 2001 From: Yongjie Zhao Date: Tue, 21 Jun 2022 20:37:51 +0800 Subject: [PATCH 64/71] fix: should raise exception when apply a categorical axis (#20451) --- superset/common/query_context_processor.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/superset/common/query_context_processor.py b/superset/common/query_context_processor.py index 003c174c2a170..8078868493b60 100644 --- a/superset/common/query_context_processor.py +++ b/superset/common/query_context_processor.py @@ -322,6 +322,16 @@ def processing_time_offsets( # pylint: disable=too-many-locals # multi-dimensional charts granularity = query_object.granularity index = granularity if granularity in df.columns else DTTM_ALIAS + if not pd.api.types.is_datetime64_any_dtype( + offset_metrics_df.get(index) + ): + raise QueryObjectValidationError( + _( + "A time column must be specified " + "when using a Time Comparison." + ) + ) + offset_metrics_df[index] = offset_metrics_df[index] - DateOffset( **normalize_time_delta(offset) ) From 9fad26fa1919fceda4abdfce0b973d536b42b6af Mon Sep 17 00:00:00 2001 From: Yongjie Zhao Date: Tue, 21 Jun 2022 20:38:58 +0800 Subject: [PATCH 65/71] fix: suppress translation warning in jest (#20404) --- superset-frontend/package-lock.json | 14 ++++++++------ .../packages/superset-ui-core/package.json | 1 + .../superset-ui-core/src/translation/Translator.ts | 2 +- .../test/translation/Translator.test.ts | 2 ++ superset-frontend/spec/helpers/shim.ts | 2 ++ superset-frontend/tsconfig.json | 3 ++- 6 files changed, 16 insertions(+), 8 deletions(-) diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 0ec9482e2d2b3..c0b831b35d839 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -16970,9 +16970,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "17.0.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.21.tgz", - "integrity": "sha512-DBZCJbhII3r90XbQxI8Y9IjjiiOGlZ0Hr32omXIZvwwZ7p4DMMXGrKXVyPfuoBOri9XNtL0UK69jYIBIsRX3QQ==" + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.0.tgz", + "integrity": "sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA==" }, "node_modules/@types/node-fetch": { "version": "2.6.1", @@ -54322,6 +54322,7 @@ "@types/fetch-mock": "^7.3.3", "@types/lodash": "^4.14.149", "@types/math-expression-evaluator": "^1.2.1", + "@types/node": "^18.0.0", "@types/prop-types": "^15.7.2", "@types/rison": "0.0.6", "@types/seedrandom": "^2.4.28", @@ -67442,6 +67443,7 @@ "@types/fetch-mock": "^7.3.3", "@types/lodash": "^4.14.149", "@types/math-expression-evaluator": "^1.2.1", + "@types/node": "^18.0.0", "@types/prop-types": "^15.7.2", "@types/rison": "0.0.6", "@types/seedrandom": "^2.4.28", @@ -69036,9 +69038,9 @@ "dev": true }, "@types/node": { - "version": "17.0.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.21.tgz", - "integrity": "sha512-DBZCJbhII3r90XbQxI8Y9IjjiiOGlZ0Hr32omXIZvwwZ7p4DMMXGrKXVyPfuoBOri9XNtL0UK69jYIBIsRX3QQ==" + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.0.tgz", + "integrity": "sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA==" }, "@types/node-fetch": { "version": "2.6.1", diff --git a/superset-frontend/packages/superset-ui-core/package.json b/superset-frontend/packages/superset-ui-core/package.json index 424f3b877620c..1958fe1a49fae 100644 --- a/superset-frontend/packages/superset-ui-core/package.json +++ b/superset-frontend/packages/superset-ui-core/package.json @@ -40,6 +40,7 @@ "@types/d3-time-format": "^2.1.0", "@types/lodash": "^4.14.149", "@types/math-expression-evaluator": "^1.2.1", + "@types/node": "^18.0.0", "@types/rison": "0.0.6", "@types/seedrandom": "^2.4.28", "@types/fetch-mock": "^7.3.3", diff --git a/superset-frontend/packages/superset-ui-core/src/translation/Translator.ts b/superset-frontend/packages/superset-ui-core/src/translation/Translator.ts index 823638ceec952..d4a6982c8d32b 100644 --- a/superset-frontend/packages/superset-ui-core/src/translation/Translator.ts +++ b/superset-frontend/packages/superset-ui-core/src/translation/Translator.ts @@ -56,7 +56,7 @@ export default class Translator { */ addTranslation(key: string, texts: ReadonlyArray) { const translations = this.i18n.options.locale_data.superset; - if (key in translations) { + if (process.env.WEBPACK_MODE !== 'test' && key in translations) { logging.warn(`Duplicate translation key "${key}", will override.`); } translations[key] = texts; diff --git a/superset-frontend/packages/superset-ui-core/test/translation/Translator.test.ts b/superset-frontend/packages/superset-ui-core/test/translation/Translator.test.ts index 9466294aca7a2..df718afe41597 100644 --- a/superset-frontend/packages/superset-ui-core/test/translation/Translator.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/translation/Translator.test.ts @@ -41,10 +41,12 @@ describe('Translator', () => { spy.mockImplementation((info: any) => { throw new Error(info); }); + process.env.WEBPACK_MODE = 'production'; }); afterAll(() => { spy.mockRestore(); + process.env.WEBPACK_MODE = 'test'; }); describe('new Translator(config)', () => { diff --git a/superset-frontend/spec/helpers/shim.ts b/superset-frontend/spec/helpers/shim.ts index 2d3e943b73519..7955ee15e0f0f 100644 --- a/superset-frontend/spec/helpers/shim.ts +++ b/superset-frontend/spec/helpers/shim.ts @@ -81,3 +81,5 @@ setupSupersetClient(); jest.mock('src/hooks/useTabId', () => ({ useTabId: () => 1, })); + +process.env.WEBPACK_MODE = 'test'; diff --git a/superset-frontend/tsconfig.json b/superset-frontend/tsconfig.json index 818147ca7cc87..9dba1c3f89b23 100644 --- a/superset-frontend/tsconfig.json +++ b/superset-frontend/tsconfig.json @@ -29,7 +29,8 @@ "types": [ "@emotion/react/types/css-prop", "jest", - "@testing-library/jest-dom" + "@testing-library/jest-dom", + "@types/node" ], /* Emit */ From 1ae935379fa8f1f5043205f218d7c1af93fae053 Mon Sep 17 00:00:00 2001 From: smileydev <47900232+prosdev0107@users.noreply.github.com> Date: Tue, 21 Jun 2022 08:58:50 -0400 Subject: [PATCH 66/71] fix(chart & table): make to prevent dates from wrapping (#20384) --- superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx index 3b037cd0c15d5..73a639df01361 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx +++ b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx @@ -394,6 +394,7 @@ export default function TableChart( colorPositiveNegative, }) : undefined)}; + white-space: ${value instanceof Date ? 'nowrap' : undefined}; `; const cellProps = { From 5afeba34bd72526844d0f71764309a6669d96c5a Mon Sep 17 00:00:00 2001 From: Diego Medina Date: Tue, 21 Jun 2022 10:06:52 -0300 Subject: [PATCH 67/71] fix(viz): BigQuery time grain 'minute'/'second' throws an error (#20350) --- superset/db_engine_specs/bigquery.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/superset/db_engine_specs/bigquery.py b/superset/db_engine_specs/bigquery.py index e33457c79abb1..51fd710ab3352 100644 --- a/superset/db_engine_specs/bigquery.py +++ b/superset/db_engine_specs/bigquery.py @@ -123,8 +123,12 @@ class BigQueryEngineSpec(BaseEngineSpec): _time_grain_expressions = { None: "{col}", - "PT1S": "{func}({col}, SECOND)", - "PT1M": "{func}({col}, MINUTE)", + "PT1S": "CAST(TIMESTAMP_SECONDS(" + "UNIX_SECONDS(CAST({col} AS TIMESTAMP))" + ") AS {type})", + "PT1M": "CAST(TIMESTAMP_SECONDS(" + "60 * DIV(UNIX_SECONDS(CAST({col} AS TIMESTAMP)), 60)" + ") AS {type})", "PT5M": "CAST(TIMESTAMP_SECONDS(" "5*60 * DIV(UNIX_SECONDS(CAST({col} AS TIMESTAMP)), 5*60)" ") AS {type})", From f3b289d3c333fe2351e9fbac6fa85b875cb1897c Mon Sep 17 00:00:00 2001 From: Beto Dealmeida Date: Tue, 21 Jun 2022 06:10:46 -0700 Subject: [PATCH 68/71] fix: ensure column name in description is string (#20340) * fix: ensure column name in description is string * Add unit test --- superset/result_set.py | 17 +++++++- superset/superset_typing.py | 8 +++- tests/unit_tests/result_set_test.py | 67 +++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 tests/unit_tests/result_set_test.py diff --git a/superset/result_set.py b/superset/result_set.py index 008699d683447..725bf1449cc79 100644 --- a/superset/result_set.py +++ b/superset/result_set.py @@ -71,6 +71,19 @@ def destringify(obj: str) -> Any: return json.loads(obj) +def convert_to_string(value: Any) -> str: + """ + Used to ensure column names from the cursor description are strings. + """ + if isinstance(value, str): + return value + + if isinstance(value, bytes): + return value.decode("utf-8") + + return str(value) + + class SupersetResultSet: def __init__( # pylint: disable=too-many-locals self, @@ -88,7 +101,9 @@ def __init__( # pylint: disable=too-many-locals if cursor_description: # get deduped list of column names - column_names = dedup([col[0] for col in cursor_description]) + column_names = dedup( + [convert_to_string(col[0]) for col in cursor_description] + ) # fix cursor descriptor with the deduped names deduped_cursor_desc = [ diff --git a/superset/superset_typing.py b/superset/superset_typing.py index 1af04494d0c95..ae8787d1c6913 100644 --- a/superset/superset_typing.py +++ b/superset/superset_typing.py @@ -69,7 +69,13 @@ class ResultSetColumnType(TypedDict): CacheConfig = Dict[str, Any] DbapiDescriptionRow = Tuple[ - str, str, Optional[str], Optional[str], Optional[int], Optional[int], bool + Union[str, bytes], + str, + Optional[str], + Optional[str], + Optional[int], + Optional[int], + bool, ] DbapiDescription = Union[List[DbapiDescriptionRow], Tuple[DbapiDescriptionRow, ...]] DbapiResult = Sequence[Union[List[Any], Tuple[Any, ...]]] diff --git a/tests/unit_tests/result_set_test.py b/tests/unit_tests/result_set_test.py new file mode 100644 index 0000000000000..80d7ced61ecd0 --- /dev/null +++ b/tests/unit_tests/result_set_test.py @@ -0,0 +1,67 @@ +# 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. + +# pylint: disable=import-outside-toplevel, unused-argument + + +def test_column_names_as_bytes(app_context: None) -> None: + """ + Test that we can handle column names as bytes. + """ + from superset.db_engine_specs.redshift import RedshiftEngineSpec + from superset.result_set import SupersetResultSet + + data = ( + [ + "2016-01-26", + 392.002014, + 397.765991, + 390.575012, + 392.153015, + 392.153015, + 58147000, + ], + [ + "2016-01-27", + 392.444, + 396.842987, + 391.782013, + 394.971985, + 394.971985, + 47424400, + ], + ) + description = [ + (b"date", 1043, None, None, None, None, None), + (b"open", 701, None, None, None, None, None), + (b"high", 701, None, None, None, None, None), + (b"low", 701, None, None, None, None, None), + (b"close", 701, None, None, None, None, None), + (b"adj close", 701, None, None, None, None, None), + (b"volume", 20, None, None, None, None, None), + ] + result_set = SupersetResultSet(data, description, RedshiftEngineSpec) # type: ignore + + assert ( + result_set.to_pandas_df().to_markdown() + == """ +| | date | open | high | low | close | adj close | volume | +|---:|:-----------|--------:|--------:|--------:|--------:|------------:|---------:| +| 0 | 2016-01-26 | 392.002 | 397.766 | 390.575 | 392.153 | 392.153 | 58147000 | +| 1 | 2016-01-27 | 392.444 | 396.843 | 391.782 | 394.972 | 394.972 | 47424400 | + """.strip() + ) From 44c5e2879b912b33c902b955eebdaa52ea6e5354 Mon Sep 17 00:00:00 2001 From: Cody Leff Date: Tue, 21 Jun 2022 11:01:43 -0600 Subject: [PATCH 69/71] chore(newchart): update chart creation dataset selection help text, styles (#20369) * Update dataset selection help text. * Update 'Create a new chart' flow styles. * Add support for linking directly to Create Dataset modal via URL hash. * Add support for linking directly to Create Dataset modal via URL hash. * Update dataset help text to not include spaces in translated strings and only include an 'Add dataset' link when user has permission to add dataset. * Clean up test file Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com> Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com> --- .../src/addSlice/AddSliceContainer.test.tsx | 114 ++++++++++++------ .../src/addSlice/AddSliceContainer.tsx | 99 +++++++++++---- superset-frontend/src/addSlice/App.tsx | 2 +- .../CRUD/data/dataset/DatasetList.test.jsx | 8 +- .../views/CRUD/data/dataset/DatasetList.tsx | 26 +++- superset/views/chart/views.py | 2 +- 6 files changed, 183 insertions(+), 68 deletions(-) diff --git a/superset-frontend/src/addSlice/AddSliceContainer.test.tsx b/superset-frontend/src/addSlice/AddSliceContainer.test.tsx index 00e7276a5864c..6187f574867c4 100644 --- a/superset-frontend/src/addSlice/AddSliceContainer.test.tsx +++ b/superset-frontend/src/addSlice/AddSliceContainer.test.tsx @@ -27,61 +27,97 @@ import AddSliceContainer, { import VizTypeGallery from 'src/explore/components/controls/VizTypeControl/VizTypeGallery'; import { styledMount as mount } from 'spec/helpers/theming'; import { act } from 'spec/helpers/testing-library'; +import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes'; const datasource = { value: '1', label: 'table', }; -describe('AddSliceContainer', () => { - let wrapper: ReactWrapper< +const mockUser: UserWithPermissionsAndRoles = { + createdOn: '2021-04-27T18:12:38.952304', + email: 'admin', + firstName: 'admin', + isActive: true, + lastName: 'admin', + permissions: {}, + roles: { Admin: Array(173) }, + userId: 1, + username: 'admin', + isAnonymous: false, +}; + +const mockUserWithDatasetWrite: UserWithPermissionsAndRoles = { + createdOn: '2021-04-27T18:12:38.952304', + email: 'admin', + firstName: 'admin', + isActive: true, + lastName: 'admin', + permissions: {}, + roles: { Admin: [['can_write', 'Dataset']] }, + userId: 1, + username: 'admin', + isAnonymous: false, +}; + +async function getWrapper(user = mockUser) { + const wrapper = mount() as ReactWrapper< AddSliceContainerProps, AddSliceContainerState, AddSliceContainer >; + await act(() => new Promise(resolve => setTimeout(resolve, 0))); + return wrapper; +} - beforeEach(async () => { - wrapper = mount() as ReactWrapper< - AddSliceContainerProps, - AddSliceContainerState, - AddSliceContainer - >; - // suppress a warning caused by some unusual async behavior in Icon - await act(() => new Promise(resolve => setTimeout(resolve, 0))); - }); +test('renders a select and a VizTypeControl', async () => { + const wrapper = await getWrapper(); + expect(wrapper.find(Select)).toExist(); + expect(wrapper.find(VizTypeGallery)).toExist(); +}); - it('renders a select and a VizTypeControl', () => { - expect(wrapper.find(Select)).toExist(); - expect(wrapper.find(VizTypeGallery)).toExist(); - }); +test('renders dataset help text when user lacks dataset write permissions', async () => { + const wrapper = await getWrapper(); + expect(wrapper.find('[data-test="dataset-write"]')).not.toExist(); + expect(wrapper.find('[data-test="no-dataset-write"]')).toExist(); +}); - it('renders a button', () => { - expect(wrapper.find(Button)).toExist(); - }); +test('renders dataset help text when user has dataset write permissions', async () => { + const wrapper = await getWrapper(mockUserWithDatasetWrite); + expect(wrapper.find('[data-test="dataset-write"]')).toExist(); + expect(wrapper.find('[data-test="no-dataset-write"]')).not.toExist(); +}); - it('renders a disabled button if no datasource is selected', () => { - expect( - wrapper.find(Button).find({ disabled: true }).hostNodes(), - ).toHaveLength(1); - }); +test('renders a button', async () => { + const wrapper = await getWrapper(); + expect(wrapper.find(Button)).toExist(); +}); - it('renders an enabled button if datasource and viz type is selected', () => { - wrapper.setState({ - datasource, - visType: 'table', - }); - expect( - wrapper.find(Button).find({ disabled: true }).hostNodes(), - ).toHaveLength(0); +test('renders a disabled button if no datasource is selected', async () => { + const wrapper = await getWrapper(); + expect( + wrapper.find(Button).find({ disabled: true }).hostNodes(), + ).toHaveLength(1); +}); + +test('renders an enabled button if datasource and viz type are selected', async () => { + const wrapper = await getWrapper(); + wrapper.setState({ + datasource, + visType: 'table', }); + expect( + wrapper.find(Button).find({ disabled: true }).hostNodes(), + ).toHaveLength(0); +}); - it('formats explore url', () => { - wrapper.setState({ - datasource, - visType: 'table', - }); - const formattedUrl = - '/superset/explore/?form_data=%7B%22viz_type%22%3A%22table%22%2C%22datasource%22%3A%221%22%7D'; - expect(wrapper.instance().exploreUrl()).toBe(formattedUrl); +test('formats Explore url', async () => { + const wrapper = await getWrapper(); + wrapper.setState({ + datasource, + visType: 'table', }); + const formattedUrl = + '/superset/explore/?form_data=%7B%22viz_type%22%3A%22table%22%2C%22datasource%22%3A%221%22%7D'; + expect(wrapper.instance().exploreUrl()).toBe(formattedUrl); }); diff --git a/superset-frontend/src/addSlice/AddSliceContainer.tsx b/superset-frontend/src/addSlice/AddSliceContainer.tsx index fd22377314ac8..66183219adbd4 100644 --- a/superset-frontend/src/addSlice/AddSliceContainer.tsx +++ b/superset-frontend/src/addSlice/AddSliceContainer.tsx @@ -24,12 +24,13 @@ import { URL_PARAMS } from 'src/constants'; import { isNullish } from 'src/utils/common'; import Button from 'src/components/Button'; import { Select, Steps } from 'src/components'; -import { FormLabel } from 'src/components/Form'; import { Tooltip } from 'src/components/Tooltip'; import VizTypeGallery, { MAX_ADVISABLE_VIZ_GALLERY_WIDTH, } from 'src/explore/components/controls/VizTypeControl/VizTypeGallery'; +import findPermission from 'src/dashboard/util/findPermission'; +import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes'; type Dataset = { id: number; @@ -38,11 +39,14 @@ type Dataset = { datasource_type: string; }; -export type AddSliceContainerProps = {}; +export type AddSliceContainerProps = { + user: UserWithPermissionsAndRoles; +}; export type AddSliceContainerState = { datasource?: { label: string; value: string }; visType: string | null; + canCreateDataset: boolean; }; const ESTIMATED_NAV_HEIGHT = 56; @@ -73,7 +77,6 @@ const StyledContainer = styled.div` display: flex; flex-direction: row; align-items: center; - margin-bottom: ${theme.gridUnit * 2}px; & > div { min-width: 200px; @@ -180,6 +183,24 @@ const StyledLabel = styled.span` `} `; +const StyledStepTitle = styled.span` + ${({ + theme: { + typography: { sizes, weights }, + }, + }) => ` + font-size: ${sizes.m}px; + font-weight: ${weights.bold}; + `} +`; + +const StyledStepDescription = styled.div` + ${({ theme: { gridUnit } }) => ` + margin-top: ${gridUnit * 4}px; + margin-bottom: ${gridUnit * 3}px; + `} +`; + export default class AddSliceContainer extends React.PureComponent< AddSliceContainerProps, AddSliceContainerState @@ -188,6 +209,11 @@ export default class AddSliceContainer extends React.PureComponent< super(props); this.state = { visType: null, + canCreateDataset: findPermission( + 'can_write', + 'Dataset', + props.user.roles, + ), }; this.changeDatasource = this.changeDatasource.bind(this); @@ -276,15 +302,49 @@ export default class AddSliceContainer extends React.PureComponent< render() { const isButtonDisabled = this.isBtnDisabled(); + const datasetHelpText = this.state.canCreateDataset ? ( + + + {t('Add a dataset')} + + {` ${t('or')} `} + + {`${t('view instructions')} `} + + + . + + ) : ( + + + {`${t('View instructions')} `} + + + . + + ); + return (

{t('Create a new chart')}

{t('Choose a dataset')}} + title={{t('Choose a dataset')}} status={this.state.datasource?.value ? 'finish' : 'process'} description={ -
+ {request}

))}
+ ); }; diff --git a/superset-frontend/src/components/Select/Select.test.tsx b/superset-frontend/src/components/Select/Select.test.tsx index 37a39204369f6..97aec1c081fad 100644 --- a/superset-frontend/src/components/Select/Select.test.tsx +++ b/superset-frontend/src/components/Select/Select.test.tsx @@ -16,10 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import React from 'react'; +import React, { RefObject } from 'react'; import { render, screen, waitFor, within } from 'spec/helpers/testing-library'; import userEvent from '@testing-library/user-event'; import { Select } from 'src/components'; +import { SelectRef } from './Select'; const ARIA_LABEL = 'Test'; const NEW_OPTION = 'Kyle'; @@ -813,6 +814,21 @@ test('async - fires a new request if all values have not been fetched', async () expect(mock).toHaveBeenCalledTimes(2); }); +test('async - requests the options again after clearing the cache', async () => { + const ref: RefObject = { current: null }; + const mock = jest.fn(loadOptions); + const pageSize = OPTIONS.length; + render( +