From a044d381819da9d36eb72682b9d31fade6cb769d Mon Sep 17 00:00:00 2001 From: Ville Brofeldt <33317356+villebro@users.noreply.github.com> Date: Wed, 10 Jan 2024 11:12:01 -0800 Subject: [PATCH 001/114] chore: bump prophet to 1.1.5 (#26431) (cherry picked from commit a1993566c417c7e90db03676ea222571562c70d1) --- requirements/base.txt | 21 ++++----------------- requirements/development.txt | 2 +- requirements/testing.txt | 8 +++----- setup.py | 12 ++++++------ 4 files changed, 14 insertions(+), 29 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index df8f3ea5572fa..2c20094f72aa0 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -72,8 +72,6 @@ colorama==0.4.6 # via # apache-superset # flask-appbuilder -convertdate==2.4.0 - # via holidays cron-descriptor==1.2.24 # via apache-superset croniter==1.0.15 @@ -143,16 +141,12 @@ geographiclib==1.52 geopy==2.2.0 # via apache-superset greenlet==2.0.2 - # via - # shillelagh - # sqlalchemy + # via shillelagh gunicorn==21.2.0 # via apache-superset hashids==1.3.1 # via apache-superset -hijri-converter==2.3.1 - # via holidays -holidays==0.23 +holidays==0.25 # via apache-superset humanize==3.11.0 # via apache-superset @@ -161,10 +155,7 @@ idna==3.2 # email-validator # requests importlib-metadata==6.6.0 - # via - # apache-superset - # flask - # shillelagh + # via apache-superset importlib-resources==5.12.0 # via limits isodate==0.6.0 @@ -263,8 +254,6 @@ pyjwt==2.4.0 # apache-superset # flask-appbuilder # flask-jwt-extended -pymeeus==0.5.12 - # via convertdate pynacl==1.5.0 # via paramiko pyparsing==3.0.6 @@ -387,9 +376,7 @@ wtforms-json==0.3.5 xlsxwriter==3.0.7 # via apache-superset zipp==3.15.0 - # via - # importlib-metadata - # importlib-resources + # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: # setuptools diff --git a/requirements/development.txt b/requirements/development.txt index a73e3a70c5c27..ca80cd60ede93 100644 --- a/requirements/development.txt +++ b/requirements/development.txt @@ -72,7 +72,7 @@ pexpect==4.8.0 # via ipython pickleshare==0.7.5 # via ipython -pillow==9.5.0 +pillow==10.2.0 # via apache-superset progress==1.6 # via -r requirements/development.in diff --git a/requirements/testing.txt b/requirements/testing.txt index c1f6e55d1293a..3bf3c78d03728 100644 --- a/requirements/testing.txt +++ b/requirements/testing.txt @@ -24,6 +24,8 @@ db-dtypes==1.1.1 # via pandas-gbq docker==6.1.1 # via -r requirements/testing.in +exceptiongroup==1.1.1 + # via pytest ephem==4.1.4 # via lunarcalendar flask-testing==0.8.1 @@ -83,8 +85,6 @@ jsonschema-spec==0.1.4 # via openapi-spec-validator kiwisolver==1.4.4 # via matplotlib -lunarcalendar==0.0.9 - # via prophet matplotlib==3.7.1 # via prophet oauthlib==3.2.2 @@ -101,7 +101,7 @@ pathable==0.4.3 # via jsonschema-spec playwright==1.37.0 # via apache-superset -prophet==1.1.1 +prophet==1.1.5 # via apache-superset proto-plus==1.22.2 # via @@ -138,8 +138,6 @@ rfc3339-validator==0.1.4 # via openapi-schema-validator rsa==4.9 # via google-auth -setuptools-git==1.2 - # via prophet sqlalchemy-bigquery==1.6.1 # via apache-superset statsd==4.0.1 diff --git a/setup.py b/setup.py index 563cd32342408..993c72784679a 100644 --- a/setup.py +++ b/setup.py @@ -95,7 +95,7 @@ def get_git_sha() -> str: "geopy", "gunicorn>=21.2.0, <22.0; sys_platform != 'win32'", "hashids>=1.3.1, <2", - "holidays>=0.23, <0.24", + "holidays>=0.25, <0.26", "humanize", "importlib_metadata", "isodate", @@ -188,18 +188,18 @@ def get_git_sha() -> str: "postgres": ["psycopg2-binary==2.9.6"], "presto": ["pyhive[presto]>=0.6.5"], "trino": ["trino>=0.324.0"], - "prophet": ["prophet==1.1.1"], - "redshift": ["sqlalchemy-redshift>=0.8.1, < 0.9"], - "rockset": ["rockset-sqlalchemy>=0.0.1, <1.0.0"], + "prophet": ["prophet>=1.1.5, <2"], + "redshift": ["sqlalchemy-redshift>=0.8.1, <0.9"], + "rockset": ["rockset-sqlalchemy>=0.0.1, <1"], "shillelagh": [ "shillelagh[datasetteapi,gsheetsapi,socrata,weatherapi]>=1.2.10, <2" ], "snowflake": ["snowflake-sqlalchemy>=1.2.4, <2"], "spark": [ "pyhive[hive]>=0.6.5;python_version<'3.11'", - "pyhive[hive_pure_sasl]>=0.7.0", + "pyhive[hive_pure_sasl]>=0.7", "tableschema", - "thrift>=0.14.1, <1.0.0", + "thrift>=0.14.1, <1", ], "teradata": ["teradatasql>=16.20.0.23"], "thumbnails": ["Pillow>=10.0.1, <11"], From 66dd7f5dc551880dc5f8c035c6a12060a0117136 Mon Sep 17 00:00:00 2001 From: Igor Khrol Date: Thu, 11 Jan 2024 02:37:18 +0200 Subject: [PATCH 002/114] fix: Trino - handle table not found in SQLLab (#26355) Co-authored-by: John Bodley <4567245+john-bodley@users.noreply.github.com> (cherry picked from commit 3daa038f5f6cc02b77bf7a03396fb31261d28dbd) --- superset/db_engine_specs/trino.py | 25 +++++++++++++++++++ .../unit_tests/db_engine_specs/test_trino.py | 16 ++++++++++++ 2 files changed, 41 insertions(+) diff --git a/superset/db_engine_specs/trino.py b/superset/db_engine_specs/trino.py index 6e56dbfa24d6b..1dc711880d729 100644 --- a/superset/db_engine_specs/trino.py +++ b/superset/db_engine_specs/trino.py @@ -26,6 +26,7 @@ from flask import current_app from sqlalchemy.engine.reflection import Inspector from sqlalchemy.engine.url import URL +from sqlalchemy.exc import NoSuchTableError from sqlalchemy.orm import Session from superset.constants import QUERY_CANCEL_KEY, QUERY_EARLY_CANCEL_KEY, USER_AGENT @@ -395,3 +396,27 @@ def get_columns( return base_cols return [col for base_col in base_cols for col in cls._expand_columns(base_col)] + + @classmethod + def get_indexes( + cls, + database: Database, + inspector: Inspector, + table_name: str, + schema: str | None, + ) -> list[dict[str, Any]]: + """ + Get the indexes associated with the specified schema/table. + + Trino dialect raises NoSuchTableError in get_indexes if table is empty. + + :param database: The database to inspect + :param inspector: The SQLAlchemy inspector + :param table_name: The table to inspect + :param schema: The schema to inspect + :returns: The indexes + """ + try: + return super().get_indexes(database, inspector, table_name, schema) + except NoSuchTableError: + return [] diff --git a/tests/unit_tests/db_engine_specs/test_trino.py b/tests/unit_tests/db_engine_specs/test_trino.py index 15e55fc5af62f..fd553b241c8f1 100644 --- a/tests/unit_tests/db_engine_specs/test_trino.py +++ b/tests/unit_tests/db_engine_specs/test_trino.py @@ -517,3 +517,19 @@ def test_get_columns_expand_rows(mocker: MockerFixture): ] _assert_columns_equal(actual, expected) + + +def test_get_indexes_no_table(): + from sqlalchemy.exc import NoSuchTableError + + from superset.db_engine_specs.trino import TrinoEngineSpec + + db_mock = Mock() + inspector_mock = Mock() + inspector_mock.get_indexes = Mock( + side_effect=NoSuchTableError("The specified table does not exist.") + ) + result = TrinoEngineSpec.get_indexes( + db_mock, inspector_mock, "test_table", "test_schema" + ) + assert result == [] From 3fe4d68b75ac3715e80e24580b432f8019bb6f42 Mon Sep 17 00:00:00 2001 From: Vitor Avila <96086495+Vitor-Avila@users.noreply.github.com> Date: Thu, 11 Jan 2024 16:44:12 -0300 Subject: [PATCH 003/114] fix(embedded): Hide dashboard fullscreen option for embedded context (#26412) (cherry picked from commit 494068b6325054be076e994ca06e01efdfe83aec) --- .../HeaderActionsDropdown.test.tsx | 16 ++++++++++++++++ .../Header/HeaderActionsDropdown/index.jsx | 4 +++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx index e112e7e5310de..deea296d4c04f 100644 --- a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx +++ b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/HeaderActionsDropdown.test.tsx @@ -92,6 +92,14 @@ const editModeOnWithFilterScopesProps = { }, }; +const guestUserProps = { + ...createProps(), + dashboardInfo: { + ...createProps().dashboardInfo, + userId: undefined, + }, +}; + function setup(props: HeaderDropdownProps) { return render(
@@ -134,6 +142,14 @@ test('should render the menu items in edit mode', async () => { expect(screen.getByText('Download')).toBeInTheDocument(); }); +test('should render the menu items in Embedded mode', async () => { + setup(guestUserProps); + expect(screen.getAllByRole('menuitem')).toHaveLength(3); + expect(screen.getByText('Refresh dashboard')).toBeInTheDocument(); + expect(screen.getByText('Download')).toBeInTheDocument(); + expect(screen.getByText('Set auto-refresh interval')).toBeInTheDocument(); +}); + describe('with native filters feature flag disabled', () => { beforeAll(() => { isFeatureEnabledMock = jest diff --git a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx index 4d8bcf4ee0930..b0d3fc251e9d9 100644 --- a/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx +++ b/superset-frontend/src/dashboard/components/Header/HeaderActionsDropdown/index.jsx @@ -216,6 +216,8 @@ class HeaderActionsDropdown extends React.PureComponent { const emailSubject = `${emailTitle} ${dashboardTitle}`; const emailBody = t('Check out this dashboard: '); + const isEmbedded = !dashboardInfo?.userId; + const url = getDashboardUrl({ pathname: window.location.pathname, filters: getActiveFilters(), @@ -237,7 +239,7 @@ class HeaderActionsDropdown extends React.PureComponent { {t('Refresh dashboard')} )} - {!editMode && ( + {!editMode && !isEmbedded && ( Date: Fri, 12 Jan 2024 11:18:39 -0500 Subject: [PATCH 004/114] fix(database): allow filtering by UUID (#26469) (cherry picked from commit e36c014290abe9583f4134464bc3f8e602c7b846) --- superset/databases/api.py | 1 + tests/unit_tests/databases/api_test.py | 40 ++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/superset/databases/api.py b/superset/databases/api.py index 8de84a16af6dc..62fb72943749f 100644 --- a/superset/databases/api.py +++ b/superset/databases/api.py @@ -205,6 +205,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi): "changed_by", "database_name", "expose_in_sqllab", + "uuid", ] search_filters = {"allow_file_upload": [DatabaseUploadEnabledFilter]} allowed_rel_fields = {"changed_by", "created_by"} diff --git a/tests/unit_tests/databases/api_test.py b/tests/unit_tests/databases/api_test.py index 28ca123ec66a0..cf3e64c306210 100644 --- a/tests/unit_tests/databases/api_test.py +++ b/tests/unit_tests/databases/api_test.py @@ -29,6 +29,46 @@ from sqlalchemy.orm.session import Session +def test_filter_by_uuid( + session: Session, + client: Any, + full_api_access: None, +) -> None: + """ + Test that we can filter databases by UUID. + + Note: this functionality is not used by the Superset UI, but is needed by 3rd + party tools that use the Superset API. If this tests breaks, please make sure + that the functionality is properly deprecated between major versions with + enough warning so that tools can be adapted. + """ + from superset.databases.api import DatabaseRestApi + from superset.models.core import Database + + DatabaseRestApi.datamodel.session = session + + # create table for databases + Database.metadata.create_all(session.get_bind()) # pylint: disable=no-member + session.add( + Database( + database_name="my_db", + sqlalchemy_uri="sqlite://", + uuid=UUID("7c1b7880-a59d-47cd-8bf1-f1eb8d2863cb"), + ) + ) + session.commit() + + response = client.get( + "/api/v1/database/?q=(filters:!((col:uuid,opr:eq,value:" + "%277c1b7880-a59d-47cd-8bf1-f1eb8d2863cb%27)))" + ) + assert response.status_code == 200 + + payload = response.json + assert len(payload["result"]) == 1 + assert payload["result"][0]["uuid"] == "7c1b7880-a59d-47cd-8bf1-f1eb8d2863cb" + + def test_post_with_uuid( session: Session, client: Any, From c2cd05ee4a15ca375ae93290e64ecb7edd5e0673 Mon Sep 17 00:00:00 2001 From: Geido <60598000+geido@users.noreply.github.com> Date: Tue, 16 Jan 2024 18:29:06 +0100 Subject: [PATCH 005/114] fix: RLS modal styling (#26634) (cherry picked from commit 820f4b9cf066c28b4fb448bce5025c614a979296) --- .../Form/LabeledErrorBoundInput.tsx | 4 +- .../src/components/InfoTooltip/index.tsx | 2 +- .../features/rls/RowLevelSecurityModal.tsx | 49 ++++++++++++------- .../src/pages/ChartList/index.tsx | 4 +- .../src/pages/DatasetList/index.tsx | 4 +- 5 files changed, 35 insertions(+), 28 deletions(-) diff --git a/superset-frontend/src/components/Form/LabeledErrorBoundInput.tsx b/superset-frontend/src/components/Form/LabeledErrorBoundInput.tsx index 51cf104b271ca..6b0feb7268f7e 100644 --- a/superset-frontend/src/components/Form/LabeledErrorBoundInput.tsx +++ b/superset-frontend/src/components/Form/LabeledErrorBoundInput.tsx @@ -116,9 +116,7 @@ const LabeledErrorBoundInput = ({ {label} - {hasTooltip && ( - - )} + {hasTooltip && } alertIconStyles(theme, !!errorMessage)} diff --git a/superset-frontend/src/components/InfoTooltip/index.tsx b/superset-frontend/src/components/InfoTooltip/index.tsx index ba08393f18fc8..a3ad7aa21acb3 100644 --- a/superset-frontend/src/components/InfoTooltip/index.tsx +++ b/superset-frontend/src/components/InfoTooltip/index.tsx @@ -73,7 +73,7 @@ export default function InfoTooltip({ trigger = 'hover', overlayStyle = defaultOverlayStyle, bgColor = defaultColor, - viewBox = '0 -2 24 24', + viewBox = '0 -1 24 24', }: InfoTooltipProps) { return ( `${theme.gridUnit * 3}px ${theme.gridUnit * 4}px ${theme.gridUnit * 2}px`}; - label { + label, + .control-label { + display: inline-block; font-size: ${({ theme }) => theme.typography.sizes.s}px; - color: ${({ theme }) => theme.colors.grayscale.light1}; + color: ${({ theme }) => theme.colors.grayscale.base}; + vertical-align: middle; + } + + .info-solid-small { + vertical-align: middle; + padding-bottom: ${({ theme }) => theme.gridUnit / 2}px; } `; @@ -69,7 +84,6 @@ const StyledInputContainer = styled.div` display: flex; flex-direction: column; margin: ${({ theme }) => theme.gridUnit}px; - margin-bottom: ${({ theme }) => theme.gridUnit * 4}px; .input-container { @@ -79,11 +93,6 @@ const StyledInputContainer = styled.div` > div { width: 100%; } - - label { - display: flex; - margin-right: ${({ theme }) => theme.gridUnit * 2}px; - } } input, @@ -91,17 +100,19 @@ const StyledInputContainer = styled.div` flex: 1 1 auto; } - textarea { - height: 100px; - resize: none; - } - .required { margin-left: ${({ theme }) => theme.gridUnit / 2}px; color: ${({ theme }) => theme.colors.error.base}; } `; +const StyledTextArea = styled.textarea` + height: 100px; + resize: none; + margin-top: ${({ theme }) => theme.gridUnit}px; + border: 1px solid ${({ theme }) => theme.colors.secondary.light3}; +`; + export interface RowLevelSecurityModalProps { rule: RLSObject | null; addSuccessToast: (msg: string) => void; @@ -355,9 +366,11 @@ function RowLevelSecurityModal(props: RowLevelSecurityModalProps) { onChange: ({ target }: { target: HTMLInputElement }) => onTextChange(target), }} - css={noBottomMargin} + css={noMargins} label={t('Rule Name')} data-test="rule-name-test" + tooltipText={t('The name of the rule must be unique')} + hasTooltip /> @@ -433,7 +446,7 @@ function RowLevelSecurityModal(props: RowLevelSecurityModalProps) { onChange: ({ target }: { target: HTMLInputElement }) => onTextChange(target), }} - css={noBottomMargin} + css={noMargins} label={t('Group Key')} hasTooltip tooltipText={t( @@ -454,7 +467,7 @@ function RowLevelSecurityModal(props: RowLevelSecurityModalProps) { onChange: ({ target }: { target: HTMLInputElement }) => onTextChange(target), }} - css={noBottomMargin} + css={noMargins} label={t('Clause')} hasTooltip tooltipText={t( @@ -468,7 +481,7 @@ function RowLevelSecurityModal(props: RowLevelSecurityModalProps) {
{t('Description')}
-