From 4f1a837bd3d4985157d36e4f2af0f6a2aa35ac45 Mon Sep 17 00:00:00 2001 From: Steven Liu Date: Thu, 9 Jan 2025 05:34:39 +1100 Subject: [PATCH 01/64] docs: add cover genius to the user list (#31750) Co-authored-by: Steven Liu --- RESOURCES/INTHEWILD.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RESOURCES/INTHEWILD.md b/RESOURCES/INTHEWILD.md index febfed87b891a..0cc009141013b 100644 --- a/RESOURCES/INTHEWILD.md +++ b/RESOURCES/INTHEWILD.md @@ -47,6 +47,7 @@ Join our growing community! - [Unit](https://www.unit.co/about-us) [@amitmiran137] - [Wise](https://wise.com) [@koszti] - [Xendit](https://xendit.co/) [@LieAlbertTriAdrian] +- [Cover Genius](https://covergenius.com/) ### Gaming - [Popoko VM Games Studio](https://popoko.live) From f29eafd0444b37ef23fed6a2e10b0b32f839c57f Mon Sep 17 00:00:00 2001 From: mujibishola Date: Wed, 8 Jan 2025 23:57:38 +0100 Subject: [PATCH 02/64] docs: add Remita to list (#31756) --- RESOURCES/INTHEWILD.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RESOURCES/INTHEWILD.md b/RESOURCES/INTHEWILD.md index 0cc009141013b..7a19f37abdbd8 100644 --- a/RESOURCES/INTHEWILD.md +++ b/RESOURCES/INTHEWILD.md @@ -43,6 +43,7 @@ Join our growing community! - [Capital Service S.A.](https://capitalservice.pl) [@pkonarzewski] - [Clark.de](https://clark.de/) - [KarrotPay](https://www.daangnpay.com/) +- [Remita](https://remita.net) [@mujibishola] - [Taveo](https://www.taveo.com) [@codek] - [Unit](https://www.unit.co/about-us) [@amitmiran137] - [Wise](https://wise.com) [@koszti] From e4b3ecd3723e95c8b20372f65d837ee546caaa27 Mon Sep 17 00:00:00 2001 From: Beto Dealmeida Date: Wed, 8 Jan 2025 22:11:28 -0500 Subject: [PATCH 03/64] feat: push predicates into virtual datasets (#31486) --- superset/config.py | 2 + superset/connectors/sqla/models.py | 6 +- superset/db_engine_specs/base.py | 2 +- superset/models/core.py | 8 ++ superset/models/helpers.py | 7 +- superset/sql/parse.py | 40 +++++++++ tests/unit_tests/db_engine_specs/test_base.py | 2 +- .../db_engine_specs/test_bigquery.py | 2 +- tests/unit_tests/models/core_test.py | 84 +++++++++++++++++++ tests/unit_tests/sql/parse_tests.py | 43 ++++++++++ 10 files changed, 191 insertions(+), 5 deletions(-) diff --git a/superset/config.py b/superset/config.py index 5d03016298aae..f36ffade3cfed 100644 --- a/superset/config.py +++ b/superset/config.py @@ -517,6 +517,8 @@ class D3TimeFormat(TypedDict, total=False): # 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, + # Try to optimize SQL queries — for now only predicate pushdown is supported. + "OPTIMIZE_SQL": False, # When impersonating a user, use the email prefix instead of the username "IMPERSONATE_WITH_EMAIL_PREFIX": False, # Enable caching per impersonation key (e.g username) in a datasource where user diff --git a/superset/connectors/sqla/models.py b/superset/connectors/sqla/models.py index ac3a75481c02f..bb27678717196 100644 --- a/superset/connectors/sqla/models.py +++ b/superset/connectors/sqla/models.py @@ -1568,7 +1568,11 @@ def adhoc_column_to_sqla( # pylint: disable=too-many-locals # probe adhoc column type tbl, _ = self.get_from_clause(template_processor) qry = sa.select([sqla_column]).limit(1).select_from(tbl) - sql = self.database.compile_sqla_query(qry) + sql = self.database.compile_sqla_query( + qry, + catalog=self.catalog, + schema=self.schema, + ) col_desc = get_columns_description( self.database, self.catalog, diff --git a/superset/db_engine_specs/base.py b/superset/db_engine_specs/base.py index cd2c318ad86a8..f239ef2019266 100644 --- a/superset/db_engine_specs/base.py +++ b/superset/db_engine_specs/base.py @@ -1701,7 +1701,7 @@ def select_star( # pylint: disable=too-many-arguments ) if partition_query is not None: qry = partition_query - sql = database.compile_sqla_query(qry) + sql = database.compile_sqla_query(qry, table.catalog, table.schema) if indent: sql = SQLScript(sql, engine=cls.engine).format() return sql diff --git a/superset/models/core.py b/superset/models/core.py index afabea8f9065a..f71e5c5b6334e 100755 --- a/superset/models/core.py +++ b/superset/models/core.py @@ -74,6 +74,7 @@ ) from superset.models.helpers import AuditMixinNullable, ImportExportMixin, UUIDMixin from superset.result_set import SupersetResultSet +from superset.sql.parse import SQLScript from superset.sql_parse import Table from superset.superset_typing import ( DbapiDescription, @@ -740,6 +741,7 @@ def compile_sqla_query( qry: Select, catalog: str | None = None, schema: str | None = None, + is_virtual: bool = False, ) -> str: with self.get_sqla_engine(catalog=catalog, schema=schema) as engine: sql = str(qry.compile(engine, compile_kwargs={"literal_binds": True})) @@ -748,6 +750,12 @@ def compile_sqla_query( if engine.dialect.identifier_preparer._double_percents: # noqa sql = sql.replace("%%", "%") + # for nwo we only optimize queries on virtual datasources, since the only + # optimization available is predicate pushdown + if is_feature_enabled("OPTIMIZE_SQL") and is_virtual: + script = SQLScript(sql, self.db_engine_spec.engine).optimize() + sql = script.format() + return sql def select_star( # pylint: disable=too-many-arguments diff --git a/superset/models/helpers.py b/superset/models/helpers.py index 9587ec2385efb..fb6dca6f65012 100644 --- a/superset/models/helpers.py +++ b/superset/models/helpers.py @@ -883,7 +883,12 @@ def get_query_str_extended( mutate: bool = True, ) -> QueryStringExtended: sqlaq = self.get_sqla_query(**query_obj) - sql = self.database.compile_sqla_query(sqlaq.sqla_query) + sql = self.database.compile_sqla_query( + sqlaq.sqla_query, + catalog=self.catalog, + schema=self.schema, + is_virtual=bool(self.sql), + ) sql = self._apply_cte(sql, sqlaq.cte) if mutate: diff --git a/superset/sql/parse.py b/superset/sql/parse.py index 91d7b51184f21..34ec9299d3600 100644 --- a/superset/sql/parse.py +++ b/superset/sql/parse.py @@ -17,6 +17,7 @@ from __future__ import annotations +import copy import enum import logging import re @@ -31,6 +32,7 @@ from sqlglot import exp from sqlglot.dialects.dialect import Dialect, Dialects from sqlglot.errors import ParseError +from sqlglot.optimizer.pushdown_predicates import pushdown_predicates from sqlglot.optimizer.scope import Scope, ScopeType, traverse_scope from superset.exceptions import SupersetParseError @@ -227,6 +229,12 @@ def is_mutating(self) -> bool: """ raise NotImplementedError() + def optimize(self) -> BaseSQLStatement[InternalRepresentation]: + """ + Return optimized statement. + """ + raise NotImplementedError() + def __str__(self) -> str: return self.format() @@ -431,6 +439,19 @@ def get_settings(self) -> dict[str, str | bool]: for eq in set_item.find_all(exp.EQ) } + def optimize(self) -> SQLStatement: + """ + Return optimized statement. + """ + # only optimize statements that have a custom dialect + if not self._dialect: + return SQLStatement(self._sql, self.engine, self._parsed.copy()) + + optimized = pushdown_predicates(self._parsed, dialect=self._dialect) + sql = optimized.sql(dialect=self._dialect) + + return SQLStatement(sql, self.engine, optimized) + class KQLSplitState(enum.Enum): """ @@ -589,6 +610,14 @@ def is_mutating(self) -> bool: """ return self._parsed.startswith(".") and not self._parsed.startswith(".show") + def optimize(self) -> KustoKQLStatement: + """ + Return optimized statement. + + Kusto KQL doesn't support optimization, so this method is a no-op. + """ + return KustoKQLStatement(self._sql, self.engine, self._parsed) + class SQLScript: """ @@ -643,6 +672,17 @@ def has_mutation(self) -> bool: """ return any(statement.is_mutating() for statement in self.statements) + def optimize(self) -> SQLScript: + """ + Return optimized script. + """ + script = copy.deepcopy(self) + script.statements = [ # type: ignore + statement.optimize() for statement in self.statements + ] + + return script + def extract_tables_from_statement( statement: exp.Expression, diff --git a/tests/unit_tests/db_engine_specs/test_base.py b/tests/unit_tests/db_engine_specs/test_base.py index 2644cd6e6f055..bbc3bb0edcefd 100644 --- a/tests/unit_tests/db_engine_specs/test_base.py +++ b/tests/unit_tests/db_engine_specs/test_base.py @@ -226,7 +226,7 @@ class NoLimitDBEngineSpec(BaseEngineSpec): # mock the database so we can compile the query database = mocker.MagicMock() - database.compile_sqla_query = lambda query: str( + database.compile_sqla_query = lambda query, catalog, schema: str( query.compile(dialect=sqlite.dialect()) ) diff --git a/tests/unit_tests/db_engine_specs/test_bigquery.py b/tests/unit_tests/db_engine_specs/test_bigquery.py index 458a6a0393e7d..c28ff0e46b49a 100644 --- a/tests/unit_tests/db_engine_specs/test_bigquery.py +++ b/tests/unit_tests/db_engine_specs/test_bigquery.py @@ -149,7 +149,7 @@ def test_select_star(mocker: MockerFixture) -> None: # mock the database so we can compile the query database = mocker.MagicMock() - database.compile_sqla_query = lambda query: str( + database.compile_sqla_query = lambda query, catalog, schema: str( query.compile(dialect=BigQueryDialect(), compile_kwargs={"literal_binds": True}) ) diff --git a/tests/unit_tests/models/core_test.py b/tests/unit_tests/models/core_test.py index 5bc3c86af657a..36ce618f887a5 100644 --- a/tests/unit_tests/models/core_test.py +++ b/tests/unit_tests/models/core_test.py @@ -21,9 +21,17 @@ import pytest from pytest_mock import MockerFixture +from sqlalchemy import ( + Column, + Integer, + MetaData, + select, + Table as SqlalchemyTable, +) from sqlalchemy.engine.reflection import Inspector from sqlalchemy.engine.url import make_url from sqlalchemy.orm.session import Session +from sqlalchemy.sql import Select from superset.connectors.sqla.models import SqlaTable, TableColumn from superset.errors import SupersetErrorType @@ -45,6 +53,29 @@ } +@pytest.fixture +def query() -> Select: + """ + A nested query fixture used to test query optimization. + """ + metadata = MetaData() + some_table = SqlalchemyTable( + "some_table", + metadata, + Column("a", Integer), + Column("b", Integer), + Column("c", Integer), + ) + + inner_select = select(some_table.c.a, some_table.c.b, some_table.c.c) + outer_select = select(inner_select.c.a, inner_select.c.b).where( + inner_select.c.a > 1, + inner_select.c.b == 2, + ) + + return outer_select + + def test_get_metrics(mocker: MockerFixture) -> None: """ Tests for ``get_metrics``. @@ -683,3 +714,56 @@ def test_purge_oauth2_tokens(session: Session) -> None: # make sure database was not deleted... just in case database = session.query(Database).filter_by(id=database1.id).one() assert database.name == "my_oauth2_db" + + +def test_compile_sqla_query_no_optimization(query: Select) -> None: + """ + Test the `compile_sqla_query` method. + """ + from superset.models.core import Database + + database = Database( + database_name="db", + sqlalchemy_uri="sqlite://", + ) + + space = " " + + assert ( + database.compile_sqla_query(query, is_virtual=True) + == f"""SELECT anon_1.a, anon_1.b{space} +FROM (SELECT some_table.a AS a, some_table.b AS b, some_table.c AS c{space} +FROM some_table) AS anon_1{space} +WHERE anon_1.a > 1 AND anon_1.b = 2""" + ) + + +@with_feature_flags(OPTIMIZE_SQL=True) +def test_compile_sqla_query(query: Select) -> None: + """ + Test the `compile_sqla_query` method. + """ + from superset.models.core import Database + + database = Database( + database_name="db", + sqlalchemy_uri="sqlite://", + ) + + assert ( + database.compile_sqla_query(query, is_virtual=True) + == """SELECT + anon_1.a, + anon_1.b +FROM ( + SELECT + some_table.a AS a, + some_table.b AS b, + some_table.c AS c + FROM some_table + WHERE + some_table.a > 1 AND some_table.b = 2 +) AS anon_1 +WHERE + TRUE AND TRUE""" + ) diff --git a/tests/unit_tests/sql/parse_tests.py b/tests/unit_tests/sql/parse_tests.py index 5103ef12eccf2..1eabb78e05d95 100644 --- a/tests/unit_tests/sql/parse_tests.py +++ b/tests/unit_tests/sql/parse_tests.py @@ -1070,3 +1070,46 @@ def test_is_mutating(engine: str) -> None: "with source as ( select 1 as one ) select * from source", engine=engine, ).is_mutating() + + +def test_optimize() -> None: + """ + Test that the `optimize` method works as expected. + + The SQL optimization only works with engines that have a corresponding dialect. + """ + sql = """ +SELECT anon_1.a, anon_1.b +FROM (SELECT some_table.a AS a, some_table.b AS b, some_table.c AS c +FROM some_table) AS anon_1 +WHERE anon_1.a > 1 AND anon_1.b = 2 + """ + + optimized = """SELECT + anon_1.a, + anon_1.b +FROM ( + SELECT + some_table.a AS a, + some_table.b AS b, + some_table.c AS c + FROM some_table + WHERE + some_table.a > 1 AND some_table.b = 2 +) AS anon_1 +WHERE + TRUE AND TRUE""" + + not_optimized = """ +SELECT anon_1.a, + anon_1.b +FROM + (SELECT some_table.a AS a, + some_table.b AS b, + some_table.c AS c + FROM some_table) AS anon_1 +WHERE anon_1.a > 1 + AND anon_1.b = 2""" + + assert SQLStatement(sql, "sqlite").optimize().format() == optimized + assert SQLStatement(sql, "firebolt").optimize().format() == not_optimized From 840a920abade19e4b00c36e925e0ff448fcf5b12 Mon Sep 17 00:00:00 2001 From: Maxime Beauchemin Date: Wed, 8 Jan 2025 19:12:22 -0800 Subject: [PATCH 04/64] feat: allowing print() statements to be unbuffered in docker (#31760) --- docker/.env | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker/.env b/docker/.env index 7511766569340..0ffbf331a7c38 100644 --- a/docker/.env +++ b/docker/.env @@ -15,6 +15,8 @@ # limitations under the License. # +# Allowing python to print() in docker +PYTHONUNBUFFERED=1 COMPOSE_PROJECT_NAME=superset DEV_MODE=true From 9cd3a8d5b0c4297f75515d793af0af7c74ce7611 Mon Sep 17 00:00:00 2001 From: alexandrusoare <37236580+alexandrusoare@users.noreply.github.com> Date: Thu, 9 Jan 2025 12:04:59 +0200 Subject: [PATCH 05/64] refactor(Button): Upgrade Button component to Antd5 (#31623) --- .../cypress/support/directories.ts | 10 +-- .../SaveDatasetActionButton/index.tsx | 2 +- .../src/components/Button/index.tsx | 77 ++++++++++++------- .../src/components/DropdownButton/index.tsx | 4 +- .../DropdownSelectableIcon/index.tsx | 4 +- .../src/components/IconButton/index.tsx | 3 +- .../PageHeaderWithActions/index.tsx | 2 +- .../ScopingModal/ChartsScopingListPanel.tsx | 2 +- .../useExploreAdditionalActionsMenu/index.jsx | 2 +- .../databases/DatabaseModal/index.test.tsx | 22 +++--- .../src/features/datasets/styles.ts | 2 +- superset-frontend/src/theme/index.ts | 40 ++++++++++ 12 files changed, 114 insertions(+), 56 deletions(-) diff --git a/superset-frontend/cypress-base/cypress/support/directories.ts b/superset-frontend/cypress-base/cypress/support/directories.ts index 902c4619ac940..c4e90228dd94d 100644 --- a/superset-frontend/cypress-base/cypress/support/directories.ts +++ b/superset-frontend/cypress-base/cypress/support/directories.ts @@ -106,7 +106,7 @@ export const databasesPage = { alertMessage: '.antd5-alert-message', errorField: '[role="alert"]', uploadJson: '[title="Upload JSON file"]', - chooseFile: '[class="ant-btn input-upload-btn"]', + chooseFile: '[class="antd5-btn input-upload-btn"]', additionalParameters: '[name="query_input"]', sqlAlchemyUriInput: dataTestLocator('sqlalchemy-uri-input'), advancedTab: '#rc-tabs-0-tab-2', @@ -148,7 +148,7 @@ export const sqlLabView = { examplesMenuItem: '[title="examples"]', tableInput: ':nth-child(4) > .select > :nth-child(1)', sqlEditor: '#brace-editor textarea', - saveAsButton: '.SaveQuery > .ant-btn', + saveAsButton: '.SaveQuery > .antd5-btn', saveAsModal: { footer: '.antd5-modal-footer', queryNameInput: 'input[class^="ant-input"]', @@ -195,7 +195,7 @@ export const savedQuery = { export const annotationLayersView = { emptyDescription: { description: '.ant-empty-description', - addAnnotationLayerButton: '.ant-empty-footer > .ant-btn', + addAnnotationLayerButton: '.ant-empty-footer > .antd5-btn', }, modal: { content: { @@ -434,7 +434,7 @@ export const dashboardListView = { newDashboardButton: '.css-yff34v', }, importModal: { - selectFileButton: '.ant-upload > .ant-btn > span', + selectFileButton: '.ant-upload > .antd5-btn > span', importButton: dataTestLocator('modal-confirm-button'), }, header: { @@ -588,7 +588,7 @@ export const exploreView = { rowsContainer: dataTestLocator('table-content-rows'), }, confirmModal: { - okButton: '.antd5-modal-confirm-btns .ant-btn-primary', + okButton: '.antd5-modal-confirm-btns .antd5-btn-primary', }, }, visualizationTypeModal: { diff --git a/superset-frontend/src/SqlLab/components/SaveDatasetActionButton/index.tsx b/superset-frontend/src/SqlLab/components/SaveDatasetActionButton/index.tsx index 2acd7665bfb15..4982774ed51ec 100644 --- a/superset-frontend/src/SqlLab/components/SaveDatasetActionButton/index.tsx +++ b/superset-frontend/src/SqlLab/components/SaveDatasetActionButton/index.tsx @@ -37,7 +37,7 @@ const SaveDatasetActionButton = ({ const StyledDropdownButton = styled( DropdownButton as FC, )` - &.ant-dropdown-button button.ant-btn.ant-btn-default { + &.ant-dropdown-button button.antd5-btn.antd5-btn-default { font-weight: ${theme.gridUnit * 150}; background-color: ${theme.colors.primary.light4}; color: ${theme.colors.primary.dark1}; diff --git a/superset-frontend/src/components/Button/index.tsx b/superset-frontend/src/components/Button/index.tsx index 8944ef7b81810..c38fbc15cab73 100644 --- a/superset-frontend/src/components/Button/index.tsx +++ b/superset-frontend/src/components/Button/index.tsx @@ -26,10 +26,10 @@ import { import { mix } from 'polished'; import cx from 'classnames'; -import { Button as AntdButton } from 'antd'; +import { Button as AntdButton } from 'antd-v5'; import { useTheme } from '@superset-ui/core'; import { Tooltip, TooltipProps } from 'src/components/Tooltip'; -import { ButtonProps as AntdButtonProps } from 'antd/lib/button'; +import { ButtonProps as AntdButtonProps } from 'antd-v5/lib/button'; export type OnClickHandler = MouseEventHandler; @@ -56,6 +56,25 @@ export type ButtonProps = Omit & showMarginRight?: boolean; }; +const decideType = (buttonStyle: ButtonStyle) => { + const typeMap: Record< + ButtonStyle, + 'primary' | 'default' | 'dashed' | 'link' + > = { + primary: 'primary', + danger: 'primary', + warning: 'primary', + success: 'primary', + secondary: 'default', + default: 'default', + tertiary: 'dashed', + dashed: 'dashed', + link: 'link', + }; + + return typeMap[buttonStyle]; +}; + export default function Button(props: ButtonProps) { const { tooltip, @@ -73,7 +92,7 @@ export default function Button(props: ButtonProps) { const theme = useTheme(); const { colors, transitionTiming, borderRadius, typography } = theme; - const { primary, grayscale, success, warning, error } = colors; + const { primary, grayscale, success, warning } = colors; let height = 32; let padding = 18; @@ -85,25 +104,19 @@ export default function Button(props: ButtonProps) { padding = 10; } - let backgroundColor = primary.light4; - let backgroundColorHover = mix(0.1, primary.base, primary.light4); - let backgroundColorActive = mix(0.25, primary.base, primary.light4); + let backgroundColor; + let backgroundColorHover; + let backgroundColorActive; let backgroundColorDisabled = grayscale.light2; - let color = primary.dark1; - let colorHover = color; + let color; + let colorHover; let borderWidth = 0; let borderStyle = 'none'; - let borderColor = 'transparent'; - let borderColorHover = 'transparent'; + let borderColor; + let borderColorHover; let borderColorDisabled = 'transparent'; - if (buttonStyle === 'primary') { - backgroundColor = primary.base; - backgroundColorHover = primary.dark1; - backgroundColorActive = mix(0.2, grayscale.dark2, primary.dark1); - color = grayscale.light5; - colorHover = color; - } else if (buttonStyle === 'tertiary' || buttonStyle === 'dashed') { + if (buttonStyle === 'tertiary' || buttonStyle === 'dashed') { backgroundColor = grayscale.light5; backgroundColorHover = grayscale.light5; backgroundColorActive = grayscale.light5; @@ -114,10 +127,6 @@ export default function Button(props: ButtonProps) { borderColorHover = primary.light1; borderColorDisabled = grayscale.light2; } else if (buttonStyle === 'danger') { - backgroundColor = error.base; - backgroundColorHover = mix(0.1, grayscale.light5, error.base); - backgroundColorActive = mix(0.2, grayscale.dark2, error.base); - color = grayscale.light5; colorHover = color; } else if (buttonStyle === 'warning') { backgroundColor = warning.base; @@ -135,7 +144,7 @@ export default function Button(props: ButtonProps) { backgroundColor = 'transparent'; backgroundColorHover = 'transparent'; backgroundColorActive = 'transparent'; - colorHover = primary.base; + color = primary.dark1; } const element = children as ReactElement; @@ -149,10 +158,14 @@ export default function Button(props: ButtonProps) { const firstChildMargin = showMarginRight && renderedChildren.length > 1 ? theme.gridUnit * 2 : 0; + const effectiveButtonStyle: ButtonStyle = buttonStyle ?? 'default'; + const button = ( :first-of-type': { + '& > span > :first-of-type': { marginRight: firstChildMargin, }, }} diff --git a/superset-frontend/src/components/DropdownButton/index.tsx b/superset-frontend/src/components/DropdownButton/index.tsx index 32a7739e3c308..ce44066acfb1f 100644 --- a/superset-frontend/src/components/DropdownButton/index.tsx +++ b/superset-frontend/src/components/DropdownButton/index.tsx @@ -23,8 +23,8 @@ import { styled } from '@superset-ui/core'; import { kebabCase } from 'lodash'; const StyledDropdownButton = styled.div` - .ant-btn-group { - button.ant-btn { + .antd5-btn-group { + button.antd5-btn { background-color: ${({ theme }) => theme.colors.primary.dark1}; border-color: transparent; color: ${({ theme }) => theme.colors.grayscale.light5}; diff --git a/superset-frontend/src/components/DropdownSelectableIcon/index.tsx b/superset-frontend/src/components/DropdownSelectableIcon/index.tsx index 12d23dc242332..39eb7d31e9f7d 100644 --- a/superset-frontend/src/components/DropdownSelectableIcon/index.tsx +++ b/superset-frontend/src/components/DropdownSelectableIcon/index.tsx @@ -41,10 +41,10 @@ export interface DropDownSelectableProps extends Pick { } const StyledDropdownButton = styled(DropdownButton as FC)` - button.ant-btn:first-of-type { + button.antd5-btn:first-of-type { display: none; } - > button.ant-btn:nth-of-type(2) { + > button.antd5-btn:nth-of-type(2) { display: inline-flex; background-color: transparent !important; height: unset; diff --git a/superset-frontend/src/components/IconButton/index.tsx b/superset-frontend/src/components/IconButton/index.tsx index 826d70e9f4e81..654e4089f5a9c 100644 --- a/superset-frontend/src/components/IconButton/index.tsx +++ b/superset-frontend/src/components/IconButton/index.tsx @@ -17,8 +17,7 @@ * under the License. */ import { styled } from '@superset-ui/core'; -import Button from 'src/components/Button'; -import { ButtonProps as AntdButtonProps } from 'antd/lib/button'; +import Button, { ButtonProps as AntdButtonProps } from 'src/components/Button'; import Icons from 'src/components/Icons'; import LinesEllipsis from 'react-lines-ellipsis'; diff --git a/superset-frontend/src/components/PageHeaderWithActions/index.tsx b/superset-frontend/src/components/PageHeaderWithActions/index.tsx index 014f9d97a7169..b84e67b31f939 100644 --- a/superset-frontend/src/components/PageHeaderWithActions/index.tsx +++ b/superset-frontend/src/components/PageHeaderWithActions/index.tsx @@ -35,7 +35,7 @@ export const menuTriggerStyles = (theme: SupersetTheme) => css` padding: 0; border: 1px solid ${theme.colors.primary.dark2}; - &.ant-btn > span.anticon { + &.antd5-btn > span.anticon { line-height: 0; transition: inherit; } diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/ScopingModal/ChartsScopingListPanel.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/ScopingModal/ChartsScopingListPanel.tsx index 119763a22ef3e..ad99071f20477 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/ScopingModal/ChartsScopingListPanel.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/CrossFilters/ScopingModal/ChartsScopingListPanel.tsx @@ -45,7 +45,7 @@ const AddButtonContainer = styled.div` padding-bottom: 1px; } - .ant-btn > .anticon + span { + .antd5-btn > .anticon + span { margin-left: 0; } `} diff --git a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx index f443ab750f4a2..5a3cf1ccf27a3 100644 --- a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx +++ b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx @@ -97,7 +97,7 @@ export const MenuTrigger = styled(Button)` padding: 0; border: 1px solid ${theme.colors.primary.dark2}; - &.ant-btn > span.anticon { + &.antd5-btn > span.anticon { line-height: 0; transition: inherit; } diff --git a/superset-frontend/src/features/databases/DatabaseModal/index.test.tsx b/superset-frontend/src/features/databases/DatabaseModal/index.test.tsx index bd3eb3bec9796..736990f836211 100644 --- a/superset-frontend/src/features/databases/DatabaseModal/index.test.tsx +++ b/superset-frontend/src/features/databases/DatabaseModal/index.test.tsx @@ -624,7 +624,7 @@ describe('DatabaseModal', () => { ]; visibleComponents.forEach(component => { - expect(component).toBeVisible(); + expect(component).toBeInTheDocument(); }); }); @@ -781,7 +781,7 @@ describe('DatabaseModal', () => { enableRowExpansionCheckbox, ]; visibleComponents.forEach(component => { - expect(component).toBeVisible(); + expect(component).toBeInTheDocument(); }); invisibleComponents.forEach(component => { expect(component).not.toBeVisible(); @@ -849,7 +849,7 @@ describe('DatabaseModal', () => { ]; visibleComponents.forEach(component => { - expect(component).toBeVisible(); + expect(component).toBeInTheDocument(); }); }); @@ -929,7 +929,7 @@ describe('DatabaseModal', () => { // ---------- Assertions ---------- visibleComponents.forEach(component => { - expect(component).toBeVisible(); + expect(component).toBeInTheDocument(); }); invisibleComponents.forEach(component => { expect(component).not.toBeVisible(); @@ -1137,7 +1137,7 @@ describe('DatabaseModal', () => { expect(await screen.findByText(/step 2 of 2/i)).toBeInTheDocument(); const sqlAlchemyFormStepText = screen.getByText(/step 2 of 2/i); - expect(sqlAlchemyFormStepText).toBeVisible(); + expect(sqlAlchemyFormStepText).toBeInTheDocument(); }); describe('SQL Alchemy form flow', () => { @@ -1293,7 +1293,7 @@ describe('DatabaseModal', () => { expect(await screen.findByText(/step 2 of 2/i)).toBeInTheDocument(); const SSHTunnelingToggle = screen.getByTestId('ssh-tunnel-switch'); - expect(SSHTunnelingToggle).toBeVisible(); + expect(SSHTunnelingToggle).toBeInTheDocument(); const SSHTunnelServerAddressInput = screen.queryByTestId( 'ssh-tunnel-server_address-input', ); @@ -1325,26 +1325,26 @@ describe('DatabaseModal', () => { const SSHTunnelUsePasswordInput = screen.getByTestId( 'ssh-tunnel-use_password-radio', ); - expect(SSHTunnelUsePasswordInput).toBeVisible(); + expect(SSHTunnelUsePasswordInput).toBeInTheDocument(); const SSHTunnelUsePrivateKeyInput = screen.getByTestId( 'ssh-tunnel-use_private_key-radio', ); - expect(SSHTunnelUsePrivateKeyInput).toBeVisible(); + expect(SSHTunnelUsePrivateKeyInput).toBeInTheDocument(); const SSHTunnelPasswordInput = screen.getByTestId( 'ssh-tunnel-password-input', ); // By default, we use Password as login method - expect(SSHTunnelPasswordInput).toBeVisible(); + expect(SSHTunnelPasswordInput).toBeInTheDocument(); // Change the login method to use private key userEvent.click(SSHTunnelUsePrivateKeyInput); const SSHTunnelPrivateKeyInput = screen.getByTestId( 'ssh-tunnel-private_key-input', ); - expect(SSHTunnelPrivateKeyInput).toBeVisible(); + expect(SSHTunnelPrivateKeyInput).toBeInTheDocument(); const SSHTunnelPrivateKeyPasswordInput = screen.getByTestId( 'ssh-tunnel-private_key_password-input', ); - expect(SSHTunnelPrivateKeyPasswordInput).toBeVisible(); + expect(SSHTunnelPrivateKeyPasswordInput).toBeInTheDocument(); }); }); }); diff --git a/superset-frontend/src/features/datasets/styles.ts b/superset-frontend/src/features/datasets/styles.ts index 728aa12ae42d2..108504475ae73 100644 --- a/superset-frontend/src/features/datasets/styles.ts +++ b/superset-frontend/src/features/datasets/styles.ts @@ -118,7 +118,7 @@ export const StyledLayoutFooter = styled.div` `; export const HeaderComponentStyles = styled.div` - .ant-btn { + .antd5-btn { span { margin-right: 0; } diff --git a/superset-frontend/src/theme/index.ts b/superset-frontend/src/theme/index.ts index 1b067aa1a00f5..efba95cee5ddb 100644 --- a/superset-frontend/src/theme/index.ts +++ b/superset-frontend/src/theme/index.ts @@ -19,6 +19,7 @@ import { type ThemeConfig } from 'antd-v5'; import { theme as supersetTheme } from 'src/preamble'; +import { mix } from 'polished'; import { lightAlgorithm } from './light'; export enum ThemeType { @@ -72,6 +73,45 @@ const baseConfig: ThemeConfig = { Badge: { paddingXS: supersetTheme.gridUnit * 2, }, + Button: { + defaultBg: supersetTheme.colors.primary.light4, + defaultHoverBg: mix( + 0.1, + supersetTheme.colors.primary.base, + supersetTheme.colors.primary.light4, + ), + defaultActiveBg: mix( + 0.25, + supersetTheme.colors.primary.base, + supersetTheme.colors.primary.light4, + ), + defaultColor: supersetTheme.colors.primary.dark1, + defaultHoverColor: supersetTheme.colors.primary.dark1, + defaultBorderColor: 'transparent', + defaultHoverBorderColor: 'transparent', + colorPrimaryHover: supersetTheme.colors.primary.dark1, + colorPrimaryActive: mix( + 0.2, + supersetTheme.colors.grayscale.dark2, + supersetTheme.colors.primary.dark1, + ), + primaryColor: supersetTheme.colors.grayscale.light5, + colorPrimaryTextHover: supersetTheme.colors.grayscale.light5, + colorError: supersetTheme.colors.error.base, + colorErrorHover: mix( + 0.1, + supersetTheme.colors.grayscale.light5, + supersetTheme.colors.error.base, + ), + colorErrorBg: mix( + 0.2, + supersetTheme.colors.grayscale.dark2, + supersetTheme.colors.error.base, + ), + dangerColor: supersetTheme.colors.grayscale.light5, + colorLinkHover: supersetTheme.colors.primary.base, + linkHoverBg: 'transparent', + }, Card: { paddingLG: supersetTheme.gridUnit * 6, fontWeightStrong: supersetTheme.typography.weights.medium, From 3a6fdf8bdf1f6ed9500dd4768867085652649982 Mon Sep 17 00:00:00 2001 From: Damian Pendrak Date: Thu, 9 Jan 2025 11:16:08 +0100 Subject: [PATCH 06/64] fix(sqllab): unable to update saved queries (#31639) --- .../src/SqlLab/actions/sqlLab.js | 1 + .../src/SqlLab/actions/sqlLab.test.js | 109 ++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.js b/superset-frontend/src/SqlLab/actions/sqlLab.js index 4fd7708338283..107ff660ac9af 100644 --- a/superset-frontend/src/SqlLab/actions/sqlLab.js +++ b/superset-frontend/src/SqlLab/actions/sqlLab.js @@ -1204,6 +1204,7 @@ export function popSavedQuery(saveQueryId) { schema: queryEditorProps.schema, sql: queryEditorProps.sql, templateParams: queryEditorProps.templateParams, + remoteId: queryEditorProps.remoteId, }; return dispatch(addQueryEditor(tmpAdaptedProps)); }) diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.test.js b/superset-frontend/src/SqlLab/actions/sqlLab.test.js index 142c6768d1b20..7591abfaea7ff 100644 --- a/superset-frontend/src/SqlLab/actions/sqlLab.test.js +++ b/superset-frontend/src/SqlLab/actions/sqlLab.test.js @@ -30,6 +30,9 @@ import { initialState, queryId, } from 'src/SqlLab/fixtures'; +import { SupersetClient } from '@superset-ui/core'; +import { ADD_TOAST } from 'src/components/MessageToasts/actions'; +import { ToastType } from '../../components/MessageToasts/types'; const middlewares = [thunk]; const mockStore = configureMockStore(middlewares); @@ -453,6 +456,112 @@ describe('async actions', () => { }); }); + describe('popSavedQuery', () => { + const supersetClientGetSpy = jest.spyOn(SupersetClient, 'get'); + const store = mockStore({}); + + const mockSavedQueryApiResponse = { + catalog: null, + changed_by: { + first_name: 'Superset', + id: 1, + last_name: 'Admin', + }, + changed_on: '2024-12-28T20:06:14.246743', + changed_on_delta_humanized: '8 days ago', + created_by: { + first_name: 'Superset', + id: 1, + last_name: 'Admin', + }, + database: { + database_name: 'examples', + id: 2, + }, + description: '', + id: 1, + label: 'Query 1', + schema: 'public', + sql: 'SELECT * FROM channels', + sql_tables: [ + { + catalog: null, + schema: null, + table: 'channels', + }, + ], + template_parameters: null, + }; + + const makeRequest = id => { + const request = actions.popSavedQuery(id); + const { dispatch } = store; + + return request(dispatch, () => initialState); + }; + + beforeEach(() => { + supersetClientGetSpy.mockClear(); + store.clearActions(); + }); + + afterAll(() => { + supersetClientGetSpy.mockRestore(); + }); + + it('calls API endpint with correct params', async () => { + supersetClientGetSpy.mockResolvedValue({ + json: { result: mockSavedQueryApiResponse }, + }); + + await makeRequest(123); + + expect(supersetClientGetSpy).toHaveBeenCalledWith({ + endpoint: '/api/v1/saved_query/123', + }); + }); + + it('dispatches addQueryEditor with correct params on successful API call', async () => { + supersetClientGetSpy.mockResolvedValue({ + json: { result: mockSavedQueryApiResponse }, + }); + + const expectedParams = { + name: 'Query 1', + dbId: 2, + catalog: null, + schema: 'public', + sql: 'SELECT * FROM channels', + templateParams: null, + remoteId: 1, + }; + + await makeRequest(1); + + const addQueryEditorAction = store + .getActions() + .find(action => action.type === actions.ADD_QUERY_EDITOR); + + expect(addQueryEditorAction).toBeTruthy(); + expect(addQueryEditorAction?.queryEditor).toEqual( + expect.objectContaining(expectedParams), + ); + }); + + it('should dispatch addDangerToast on API error', async () => { + supersetClientGetSpy.mockResolvedValue(new Error()); + + await makeRequest(1); + + const addToastAction = store + .getActions() + .find(action => action.type === ADD_TOAST); + + expect(addToastAction).toBeTruthy(); + expect(addToastAction?.payload?.toastType).toBe(ToastType.Danger); + }); + }); + describe('addQueryEditor', () => { it('creates new query editor', () => { expect.assertions(1); From 5acd03876baa787e283bdb6b59d44126c53ed65c Mon Sep 17 00:00:00 2001 From: "Michael S. Molina" <70410625+michael-s-molina@users.noreply.github.com> Date: Thu, 9 Jan 2025 10:59:41 -0300 Subject: [PATCH 07/64] refactor: Removes Apply to all panels filters scope configuration (#31754) --- .../FilterScope/FilterScope.test.tsx | 3 - .../FilterScope/FilterScope.tsx | 72 ++++--------------- .../FiltersConfigForm/FilterScope/types.ts | 5 -- .../FiltersConfigForm/FilterScope/utils.ts | 5 -- .../FiltersConfigForm/FiltersConfigForm.tsx | 1 - 5 files changed, 14 insertions(+), 72 deletions(-) diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/FilterScope.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/FilterScope.test.tsx index cca776af15647..d836b8255bf77 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/FilterScope.test.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/FilterScope.test.tsx @@ -82,7 +82,6 @@ describe('FilterScope', () => { it('select tree values with 1 excluded', async () => { render(); fireEvent.click(screen.getByText('Scoping')); - fireEvent.click(screen.getByLabelText('Apply to specific panels')); expect(screen.getByRole('tree')).toBeInTheDocument(); fireEvent.click(getTreeSwitcher(2)); fireEvent.click(screen.getByText('CHART_ID2')); @@ -99,7 +98,6 @@ describe('FilterScope', () => { it('select 1 value only', async () => { render(); fireEvent.click(screen.getByText('Scoping')); - fireEvent.click(screen.getByLabelText('Apply to specific panels')); expect(screen.getByRole('tree')).toBeInTheDocument(); fireEvent.click(getTreeSwitcher(2)); fireEvent.click(screen.getByText('CHART_ID2')); @@ -124,7 +122,6 @@ describe('FilterScope', () => { />, ); fireEvent.click(screen.getByText('Scoping')); - fireEvent.click(screen.getByLabelText('Apply to specific panels')); await waitFor(() => { expect(screen.getByRole('tree')).toBeInTheDocument(); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/FilterScope.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/FilterScope.tsx index 682894acc294c..d175f50bccf88 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/FilterScope.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/FilterScope.tsx @@ -17,13 +17,11 @@ * under the License. */ -import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { NativeFilterScope, styled, t } from '@superset-ui/core'; -import { Radio } from 'src/components/Radio'; -import { AntdForm, Typography } from 'src/components'; -import { ScopingType } from './types'; +import { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import { NativeFilterScope, styled } from '@superset-ui/core'; +import { AntdForm } from 'src/components'; import ScopingTree from './ScopingTree'; -import { getDefaultScopeValue, isScopingAll } from './utils'; +import { getDefaultScopeValue } from './utils'; type FilterScopeProps = { pathToFormValue?: string[]; @@ -31,7 +29,6 @@ type FilterScopeProps = { formFilterScope?: NativeFilterScope; forceUpdate: Function; filterScope?: NativeFilterScope; - formScopingType?: ScopingType; chartId?: number; initiallyExcludedCharts?: number[]; }; @@ -51,7 +48,6 @@ const CleanFormItem = styled(AntdForm.Item)` const FilterScope: FC = ({ pathToFormValue = [], - formScopingType, formFilterScope, forceUpdate, filterScope, @@ -63,25 +59,14 @@ const FilterScope: FC = ({ () => filterScope || getDefaultScopeValue(chartId, initiallyExcludedCharts), [chartId, filterScope, initiallyExcludedCharts], ); - const lastSpecificScope = useRef(initialFilterScope); - const initialScopingType = useMemo( - () => - isScopingAll(initialFilterScope, chartId) - ? ScopingType.All - : ScopingType.Specific, - [chartId, initialFilterScope], - ); const [hasScopeBeenModified, setHasScopeBeenModified] = useState(false); const onUpdateFormValues = useCallback( (formValues: any) => { - if (formScopingType === ScopingType.Specific) { - lastSpecificScope.current = formValues.scope; - } updateFormValues(formValues); setHasScopeBeenModified(true); }, - [formScopingType, updateFormValues], + [updateFormValues], ); const updateScopes = useCallback( @@ -98,49 +83,20 @@ const FilterScope: FC = ({ useEffect(() => { const updatedFormValues = { scope: initialFilterScope, - scoping: initialScopingType, }; updateScopes(updatedFormValues); - }, [initialFilterScope, initialScopingType, updateScopes]); + }, [initialFilterScope, updateScopes]); return ( - - { - const scope = - value === ScopingType.All - ? getDefaultScopeValue(chartId) - : lastSpecificScope.current; - updateFormValues({ scope }); - setHasScopeBeenModified(true); - forceUpdate(); - }} - > - {t('Apply to all panels')} - - {t('Apply to specific panels')} - - - - - {(formScopingType ?? initialScopingType) === ScopingType.Specific - ? t('Only selected panels will be affected by this filter') - : t('All panels with this column will be affected by this filter')} - - {(formScopingType ?? initialScopingType) === ScopingType.Specific && ( - - )} +