From 5747fb1e850ca994bf35bbe85d2d2bca064666f0 Mon Sep 17 00:00:00 2001 From: om pharate <72200400+ompharate@users.noreply.github.com> Date: Sat, 24 Jan 2026 02:10:07 +0530 Subject: [PATCH 01/13] feat(ListView): add pagination to card view and center row count display (#36288) --- .../src/components/Pagination/index.tsx | 24 +++++++++++ .../superset-ui-core/src/components/index.ts | 2 + .../src/components/ListView/ListView.tsx | 43 +++++++++++++++---- 3 files changed, 61 insertions(+), 8 deletions(-) create mode 100644 superset-frontend/packages/superset-ui-core/src/components/Pagination/index.tsx diff --git a/superset-frontend/packages/superset-ui-core/src/components/Pagination/index.tsx b/superset-frontend/packages/superset-ui-core/src/components/Pagination/index.tsx new file mode 100644 index 000000000000..62c2423a1d80 --- /dev/null +++ b/superset-frontend/packages/superset-ui-core/src/components/Pagination/index.tsx @@ -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. + */ +import { Pagination as AntdPagination } from 'antd'; +import type { PaginationProps as AntdPaginationProps } from 'antd'; + +export type PaginationProps = AntdPaginationProps; + +export const Pagination = AntdPagination; diff --git a/superset-frontend/packages/superset-ui-core/src/components/index.ts b/superset-frontend/packages/superset-ui-core/src/components/index.ts index 12a0504ce55e..96d17aa65471 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/index.ts +++ b/superset-frontend/packages/superset-ui-core/src/components/index.ts @@ -147,6 +147,8 @@ export { Loading, type LoadingProps } from './Loading'; export { Progress, type ProgressProps } from './Progress'; +export { Pagination, type PaginationProps } from './Pagination'; + export { Skeleton, type SkeletonProps } from './Skeleton'; export { Switch, type SwitchProps } from './Switch'; diff --git a/superset-frontend/src/components/ListView/ListView.tsx b/superset-frontend/src/components/ListView/ListView.tsx index bfc0731924ad..f81532beebe8 100644 --- a/superset-frontend/src/components/ListView/ListView.tsx +++ b/superset-frontend/src/components/ListView/ListView.tsx @@ -28,6 +28,7 @@ import { Icons, EmptyState, Loading, + Pagination, type EmptyStateProps, } from '@superset-ui/core/components'; import CardCollection from './CardCollection'; @@ -91,6 +92,7 @@ const ListViewStyles = styled.div` .row-count-container { margin-top: ${theme.sizeUnit * 2}px; color: ${theme.colorText}; + text-align: center; } `} `; @@ -446,14 +448,39 @@ export function ListView({ /> )} {viewMode === 'card' && ( - + <> + + {count > 0 && ( +
+ { + gotoPage(page - 1); + }} + size="default" + showSizeChanger={false} + showQuickJumper={false} + hideOnSinglePage + align="center" + /> +
+ {`${pageIndex * pageSize + 1}-${Math.min( + (pageIndex + 1) * pageSize, + count, + )} of ${count}`} +
+
+ )} + )} {viewMode === 'table' && ( <> From e8363cf60613e45ab2160ef846be82c319ce3e9d Mon Sep 17 00:00:00 2001 From: Jean Massucatto Date: Fri, 23 Jan 2026 17:40:38 -0300 Subject: [PATCH 02/13] fix(redshift): normalize table names to lowercase for CSV uploads (#37019) --- superset/commands/database/uploaders/base.py | 6 ++++ superset/db_engine_specs/base.py | 20 +++++++++++++ superset/db_engine_specs/redshift.py | 18 +++++++++++ .../db_engine_specs/test_redshift.py | 30 +++++++++++++++++++ 4 files changed, 74 insertions(+) diff --git a/superset/commands/database/uploaders/base.py b/superset/commands/database/uploaders/base.py index 18b4f8024f4c..f8e2b389d848 100644 --- a/superset/commands/database/uploaders/base.py +++ b/superset/commands/database/uploaders/base.py @@ -159,6 +159,12 @@ def run(self) -> None: if not self._model: return + self._table_name, self._schema = ( + self._model.db_engine_spec.normalize_table_name_for_upload( + self._table_name, self._schema + ) + ) + self._reader.read(self._file, self._model, self._table_name, self._schema) sqla_table = ( diff --git a/superset/db_engine_specs/base.py b/superset/db_engine_specs/base.py index 4113a3e8fe55..396c3805acda 100644 --- a/superset/db_engine_specs/base.py +++ b/superset/db_engine_specs/base.py @@ -1295,6 +1295,26 @@ def get_cte_query(cls, sql: str) -> str | None: return None + @classmethod + def normalize_table_name_for_upload( + cls, + table_name: str, + schema_name: str | None = None, + ) -> tuple[str, str | None]: + """ + Normalize table and schema names for file upload. + + Some databases (e.g., Redshift) fold unquoted identifiers to lowercase, + which can cause issues when the upload creates a table with one case + but metadata operations use a different case. Override this method + to normalize names according to database-specific rules. + + :param table_name: The table name to normalize + :param schema_name: The schema name to normalize (optional) + :return: Tuple of (normalized_table_name, normalized_schema_name) + """ + return table_name, schema_name + @classmethod def df_to_sql( cls, diff --git a/superset/db_engine_specs/redshift.py b/superset/db_engine_specs/redshift.py index df8ee834fc43..ea49c479dea7 100644 --- a/superset/db_engine_specs/redshift.py +++ b/superset/db_engine_specs/redshift.py @@ -183,6 +183,24 @@ class RedshiftEngineSpec(BasicParametersMixin, PostgresBaseEngineSpec): ), } + @classmethod + def normalize_table_name_for_upload( + cls, + table_name: str, + schema_name: str | None = None, + ) -> tuple[str, str | None]: + """ + Redshift folds unquoted identifiers to lowercase. + + :param table_name: The table name to normalize + :param schema_name: The schema name to normalize (optional) + :return: Tuple of (normalized_table_name, normalized_schema_name) + """ + return ( + table_name.lower(), + schema_name.lower() if schema_name else None, + ) + @classmethod def df_to_sql( cls, diff --git a/tests/unit_tests/db_engine_specs/test_redshift.py b/tests/unit_tests/db_engine_specs/test_redshift.py index 77022809075a..d733767503fd 100644 --- a/tests/unit_tests/db_engine_specs/test_redshift.py +++ b/tests/unit_tests/db_engine_specs/test_redshift.py @@ -20,6 +20,7 @@ import pytest +from superset.db_engine_specs.redshift import RedshiftEngineSpec from tests.unit_tests.db_engine_specs.utils import assert_convert_dttm from tests.unit_tests.fixtures.common import dttm # noqa: F401 @@ -49,3 +50,32 @@ def test_convert_dttm( ) assert_convert_dttm(spec, target_type, expected_result, dttm) + + +@pytest.mark.parametrize( + "table_name,schema_name,expected_table,expected_schema", + [ + ("BPO_mytest_2", "MySchema", "bpo_mytest_2", "myschema"), + ("MY_TABLE", None, "my_table", None), + ("already_lower", "lower_schema", "already_lower", "lower_schema"), + ], +) +def test_normalize_table_name_for_upload( + table_name: str, + schema_name: Optional[str], + expected_table: str, + expected_schema: Optional[str], +) -> None: + """ + Test that table and schema names are normalized to lowercase for Redshift. + + Redshift folds unquoted identifiers to lowercase, so we need to normalize + table names to ensure consistent behavior when checking table existence + and performing replace operations. + """ + normalized_table, normalized_schema = ( + RedshiftEngineSpec.normalize_table_name_for_upload(table_name, schema_name) + ) + + assert normalized_table == expected_table + assert normalized_schema == expected_schema From d6328fcb42f012c323c76832f16193c1eb7ea4e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 23 Jan 2026 12:57:59 -0800 Subject: [PATCH 03/13] chore(deps): bump mapbox-gl from 3.18.0 to 3.18.1 in /superset-frontend (#37382) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- superset-frontend/package-lock.json | 8 ++++---- superset-frontend/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 9db8f8070d1f..93a8abc29bd0 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -88,7 +88,7 @@ "json-bigint": "^1.0.0", "json-stringify-pretty-compact": "^2.0.0", "lodash": "^4.17.21", - "mapbox-gl": "^3.18.0", + "mapbox-gl": "^3.18.1", "markdown-to-jsx": "^9.6.0", "match-sorter": "^6.3.4", "memoize-one": "^5.2.1", @@ -43299,9 +43299,9 @@ "license": "MIT" }, "node_modules/mapbox-gl": { - "version": "3.18.0", - "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.18.0.tgz", - "integrity": "sha512-xMr9HUoof0qPqWrVNK+kLiPtU1ogyfR6cihGSTB4eQAzdfFuMTC7CPrbpbZK0oUKQxXI/1qvB35FXZIK7kfR9w==", + "version": "3.18.1", + "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.18.1.tgz", + "integrity": "sha512-Izc8dee2zkmb6Pn9hXFbVioPRLXJz1OFUcrvri69MhFACPU4bhLyVmhEsD9AyW1qOAP0Yvhzm60v63xdMIHPPw==", "license": "SEE LICENSE IN LICENSE.txt", "workspaces": [ "src/style-spec", diff --git a/superset-frontend/package.json b/superset-frontend/package.json index 2977ab431f5f..6178e08d723f 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -169,7 +169,7 @@ "json-bigint": "^1.0.0", "json-stringify-pretty-compact": "^2.0.0", "lodash": "^4.17.21", - "mapbox-gl": "^3.18.0", + "mapbox-gl": "^3.18.1", "markdown-to-jsx": "^9.6.0", "match-sorter": "^6.3.4", "memoize-one": "^5.2.1", From 34418d7e0b6070c1ec2ec760d817c4ae71ed6922 Mon Sep 17 00:00:00 2001 From: Christian Geier Date: Fri, 23 Jan 2026 21:58:50 +0100 Subject: [PATCH 04/13] fix(datasets): respect application root in database management link (#36986) --- .../src/features/datasets/AddDataset/LeftPanel/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/superset-frontend/src/features/datasets/AddDataset/LeftPanel/index.tsx b/superset-frontend/src/features/datasets/AddDataset/LeftPanel/index.tsx index fec2e106e1ec..8e3b2a006eb8 100644 --- a/superset-frontend/src/features/datasets/AddDataset/LeftPanel/index.tsx +++ b/superset-frontend/src/features/datasets/AddDataset/LeftPanel/index.tsx @@ -30,6 +30,7 @@ import { } from 'src/features/datasets/AddDataset/types'; import { Table } from 'src/hooks/apiResources'; import { Typography } from '@superset-ui/core/components/Typography'; +import { ensureAppRoot } from 'src/utils/pathUtils'; interface LeftPanelProps { setDataset: Dispatch>; @@ -191,7 +192,7 @@ export default function LeftPanel({ description={ {t('Manage your databases')}{' '} - + {t('here')} From 39ebf7a7ad13db32117ca1a866fe176f1164b4f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90=E1=BB=97=20Tr=E1=BB=8Dng=20H=E1=BA=A3i?= <41283691+hainenber@users.noreply.github.com> Date: Sat, 24 Jan 2026 03:59:19 +0700 Subject: [PATCH 05/13] chore(websocket): sync Node version to LTS v22 (#37102) Signed-off-by: hainenber Co-authored-by: codeant-ai-for-open-source[bot] <244253245+codeant-ai-for-open-source[bot]@users.noreply.github.com> --- superset-websocket/.nvmrc | 2 +- superset-websocket/Dockerfile | 4 ++-- superset-websocket/package-lock.json | 2 +- superset-websocket/package.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/superset-websocket/.nvmrc b/superset-websocket/.nvmrc index 4a207c55991a..42a1c98ac5a1 100644 --- a/superset-websocket/.nvmrc +++ b/superset-websocket/.nvmrc @@ -1 +1 @@ -v20.18.3 +v22.22.0 diff --git a/superset-websocket/Dockerfile b/superset-websocket/Dockerfile index ac6e4a299935..e9aabd124fa4 100644 --- a/superset-websocket/Dockerfile +++ b/superset-websocket/Dockerfile @@ -12,7 +12,7 @@ # 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 node:16-alpine AS build +FROM node:22-alpine AS build WORKDIR /home/superset-websocket @@ -22,7 +22,7 @@ RUN npm ci && \ npm run build -FROM node:16-alpine +FROM node:22-alpine ENV NODE_ENV=production WORKDIR /home/superset-websocket diff --git a/superset-websocket/package-lock.json b/superset-websocket/package-lock.json index 85e06ec13208..ea38342f3b9f 100644 --- a/superset-websocket/package-lock.json +++ b/superset-websocket/package-lock.json @@ -43,7 +43,7 @@ "typescript-eslint": "^8.53.1" }, "engines": { - "node": "^20.19.4", + "node": "^22.22.0", "npm": "^10.8.2" } }, diff --git a/superset-websocket/package.json b/superset-websocket/package.json index d5700ab22ef3..bb066bad0c89 100644 --- a/superset-websocket/package.json +++ b/superset-websocket/package.json @@ -51,7 +51,7 @@ "typescript-eslint": "^8.53.1" }, "engines": { - "node": "^20.19.4", + "node": "^22.22.0", "npm": "^10.8.2" } } From d54e227e25c229bdc2ec841c456e0e3e4d8ba786 Mon Sep 17 00:00:00 2001 From: Elena Felder <41136058+elefeint@users.noreply.github.com> Date: Fri, 23 Jan 2026 16:25:17 -0500 Subject: [PATCH 06/13] chore: update old MotherDuck duckdb version to follow the official duckdb one (#36834) --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a3317d98dc5e..70be66485280 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -160,8 +160,9 @@ hive = [ impala = ["impyla>0.16.2, <0.17"] kusto = ["sqlalchemy-kusto>=3.0.0, <4"] kylin = ["kylinpy>=2.8.1, <2.9"] -motherduck = ["duckdb==0.10.2", "duckdb-engine>=0.12.1, <0.13"] mssql = ["pymssql>=2.2.8, <3"] +# motherduck is an alias for duckdb - MotherDuck works via the duckdb driver +motherduck = ["apache-superset[duckdb]"] mysql = ["mysqlclient>=2.1.0, <3"] ocient = [ "sqlalchemy-ocient>=1.0.0", From e4f649e49cd5ebffcb0b9d928d54b1dcd8b3eefb Mon Sep 17 00:00:00 2001 From: Martyn Gigg Date: Fri, 23 Jan 2026 22:13:48 +0000 Subject: [PATCH 07/13] fix(superset-frontend): Fixes for broken functionality when an application root is defined (#36058) --- .../src/SqlLab/components/ResultSet/index.tsx | 13 +++-- .../src/explore/exploreUtils/index.js | 25 +++++----- .../src/features/home/Menu.test.tsx | 45 ++++++++++------- superset-frontend/src/features/home/Menu.tsx | 20 ++++++-- .../src/features/home/RightMenu.tsx | 2 +- superset-frontend/src/utils/assetUrl.test.ts | 48 +++++++++++++++++++ superset-frontend/src/utils/assetUrl.ts | 13 ++++- superset/security/manager.py | 25 ---------- superset/views/base.py | 6 --- 9 files changed, 128 insertions(+), 69 deletions(-) create mode 100644 superset-frontend/src/utils/assetUrl.test.ts diff --git a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx index 52290635808a..9dddf6c1c3e7 100644 --- a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx +++ b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx @@ -288,9 +288,16 @@ const ResultSet = ({ all_columns: results.columns.map(column => column.column_name), }); - const url = mountExploreUrl(null, { - [URL_PARAMS.formDataKey.name]: key, - }); + const force = false; + const includeAppRoot = openInNewWindow; + const url = mountExploreUrl( + null, + { + [URL_PARAMS.formDataKey.name]: key, + }, + force, + includeAppRoot, + ); if (openInNewWindow) { window.open(url, '_blank', 'noreferrer'); } else { diff --git a/superset-frontend/src/explore/exploreUtils/index.js b/superset-frontend/src/explore/exploreUtils/index.js index 1ebde5a3783b..e7df0249998e 100644 --- a/superset-frontend/src/explore/exploreUtils/index.js +++ b/superset-frontend/src/explore/exploreUtils/index.js @@ -78,21 +78,24 @@ export function getAnnotationJsonUrl(slice_id, force) { .toString(); } -export function getURIDirectory(endpointType = 'base') { +export function getURIDirectory(endpointType = 'base', includeAppRoot = true) { // Building the directory part of the URI - if ( - ['full', 'json', 'csv', 'query', 'results', 'samples'].includes( - endpointType, - ) - ) { - return ensureAppRoot('/superset/explore_json/'); - } - return ensureAppRoot('/explore/'); + const uri = ['full', 'json', 'csv', 'query', 'results', 'samples'].includes( + endpointType, + ) + ? '/superset/explore_json/' + : '/explore/'; + return includeAppRoot ? ensureAppRoot(uri) : uri; } -export function mountExploreUrl(endpointType, extraSearch = {}, force = false) { +export function mountExploreUrl( + endpointType, + extraSearch = {}, + force = false, + includeAppRoot = true, +) { const uri = new URI('/'); - const directory = getURIDirectory(endpointType); + const directory = getURIDirectory(endpointType, includeAppRoot); const search = uri.search(true); Object.keys(extraSearch).forEach(key => { search[key] = extraSearch[key]; diff --git a/superset-frontend/src/features/home/Menu.test.tsx b/superset-frontend/src/features/home/Menu.test.tsx index 4dd01c66e1b1..4b7e744188e7 100644 --- a/superset-frontend/src/features/home/Menu.test.tsx +++ b/superset-frontend/src/features/home/Menu.test.tsx @@ -22,6 +22,7 @@ import { render, screen, userEvent } from 'spec/helpers/testing-library'; import setupCodeOverrides from 'src/setup/setupCodeOverrides'; import { getExtensionsRegistry } from '@superset-ui/core'; import { Menu } from './Menu'; +import * as getBootstrapData from 'src/utils/getBootstrapData'; const dropdownItems = [ { @@ -238,6 +239,10 @@ const notanonProps = { }; const useSelectorMock = jest.spyOn(reactRedux, 'useSelector'); +const staticAssetsPrefixMock = jest.spyOn( + getBootstrapData, + 'staticAssetsPrefix', +); fetchMock.get( 'glob:*api/v1/database/?q=(filters:!((col:allow_file_upload,opr:upload_is_enabled,value:!t)))', @@ -247,6 +252,8 @@ fetchMock.get( beforeEach(() => { // setup a DOM element as a render target useSelectorMock.mockClear(); + // By default use empty static assets prefix + staticAssetsPrefixMock.mockReturnValue(''); }); test('should render', async () => { @@ -272,23 +279,27 @@ test('should render the navigation', async () => { expect(await screen.findByRole('navigation')).toBeInTheDocument(); }); -test('should render the brand', async () => { - useSelectorMock.mockReturnValue({ roles: user.roles }); - const { - data: { - brand: { alt, icon }, - }, - } = mockedProps; - render(, { - useRedux: true, - useQueryParams: true, - useRouter: true, - useTheme: true, - }); - expect(await screen.findByAltText(alt)).toBeInTheDocument(); - const image = screen.getByAltText(alt); - expect(image).toHaveAttribute('src', icon); -}); +test.each(['', '/myapp'])( + 'should render the brand, including app_root "%s"', + async app_root => { + staticAssetsPrefixMock.mockReturnValue(app_root); + useSelectorMock.mockReturnValue({ roles: user.roles }); + const { + data: { + brand: { alt, icon }, + }, + } = mockedProps; + render(, { + useRedux: true, + useQueryParams: true, + useRouter: true, + useTheme: true, + }); + expect(await screen.findByAltText(alt)).toBeInTheDocument(); + const image = screen.getByAltText(alt); + expect(image).toHaveAttribute('src', `${app_root}${icon}`); + }, +); test('should render the environment tag', async () => { useSelectorMock.mockReturnValue({ roles: user.roles }); diff --git a/superset-frontend/src/features/home/Menu.tsx b/superset-frontend/src/features/home/Menu.tsx index 9d8c35341bb2..96270b03ecc3 100644 --- a/superset-frontend/src/features/home/Menu.tsx +++ b/superset-frontend/src/features/home/Menu.tsx @@ -18,6 +18,8 @@ */ import { useState, useEffect } from 'react'; import { styled, css, useTheme } from '@apache-superset/core/ui'; +import { ensureStaticPrefix } from 'src/utils/assetUrl'; +import { ensureAppRoot } from 'src/utils/pathUtils'; import { getUrlParam } from 'src/utils/urlUtils'; import { MainNav, MenuItem } from '@superset-ui/core/components/Menu'; import { Tooltip, Grid, Row, Col, Image } from '@superset-ui/core/components'; @@ -287,10 +289,10 @@ export function Menu({ if (theme.brandLogoUrl) { link = ( - + @@ -303,17 +305,25 @@ export function Menu({ // Kept as is for backwards compatibility with the old theme system / superset_config.py link = ( - + ); } else { link = ( - + ); } diff --git a/superset-frontend/src/features/home/RightMenu.tsx b/superset-frontend/src/features/home/RightMenu.tsx index 05a7d0f85a6e..dcca12f03049 100644 --- a/superset-frontend/src/features/home/RightMenu.tsx +++ b/superset-frontend/src/features/home/RightMenu.tsx @@ -483,7 +483,7 @@ const RightMenu = ({ userItems.push({ key: 'info', label: ( - + {t('Info')} ), diff --git a/superset-frontend/src/utils/assetUrl.test.ts b/superset-frontend/src/utils/assetUrl.test.ts new file mode 100644 index 000000000000..ac501899b7a1 --- /dev/null +++ b/superset-frontend/src/utils/assetUrl.test.ts @@ -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 * as getBootstrapData from 'src/utils/getBootstrapData'; +import { assetUrl, ensureStaticPrefix } from './assetUrl'; + +const staticAssetsPrefixMock = jest.spyOn( + getBootstrapData, + 'staticAssetsPrefix', +); +const resourcePath = '/endpoint/img.png'; +const absoluteResourcePath = `https://cdn.domain.com/static${resourcePath}`; + +beforeEach(() => { + staticAssetsPrefixMock.mockReturnValue(''); +}); + +describe('assetUrl should prepend static asset prefix for relative paths', () => { + it.each(['', '/myapp'])("'%s' for relative path", app_root => { + staticAssetsPrefixMock.mockReturnValue(app_root); + expect(assetUrl(resourcePath)).toBe(`${app_root}${resourcePath}`); + expect(assetUrl(absoluteResourcePath)).toBe( + `${app_root}/${absoluteResourcePath}`, + ); + }); +}); + +describe('assetUrl should ignore static asset prefix for absolute URLs', () => { + it.each(['', '/myapp'])("'%s' for absolute url", app_root => { + staticAssetsPrefixMock.mockReturnValue(app_root); + expect(ensureStaticPrefix(absoluteResourcePath)).toBe(absoluteResourcePath); + }); +}); diff --git a/superset-frontend/src/utils/assetUrl.ts b/superset-frontend/src/utils/assetUrl.ts index 5e834527838c..a7fc2553b50d 100644 --- a/superset-frontend/src/utils/assetUrl.ts +++ b/superset-frontend/src/utils/assetUrl.ts @@ -23,6 +23,17 @@ import { staticAssetsPrefix } from 'src/utils/getBootstrapData'; * defined in the bootstrap data * @param path A string path to a resource */ -export function assetUrl(path: string) { +export function assetUrl(path: string): string { return `${staticAssetsPrefix()}${path.startsWith('/') ? path : `/${path}`}`; } + +/** + * Returns the path prepended with the staticAssetsPrefix if the string is a relative path else it returns + * the string as is. + * @param url_or_path A url or relative path to a resource + */ +export function ensureStaticPrefix(url_or_path: string): string { + if (url_or_path.startsWith('/')) return assetUrl(url_or_path); + + return url_or_path; +} diff --git a/superset/security/manager.py b/superset/security/manager.py index 57e541cbca69..a619b6ca2dd7 100644 --- a/superset/security/manager.py +++ b/superset/security/manager.py @@ -44,7 +44,6 @@ PermissionViewModelView, ViewMenuModelView, ) -from flask_appbuilder.widgets import ListWidget from flask_babel import lazy_gettext as _ from flask_login import AnonymousUserMixin, LoginManager from jwt.api_jwt import _jwt_global_obj @@ -111,27 +110,6 @@ class DatabaseCatalogSchema(NamedTuple): schema: str -class SupersetSecurityListWidget(ListWidget): # pylint: disable=too-few-public-methods - """ - Redeclaring to avoid circular imports - """ - - template = "superset/fab_overrides/list.html" - - -class SupersetRoleListWidget(ListWidget): # pylint: disable=too-few-public-methods - """ - Role model view from FAB already uses a custom list widget override - So we override the override - """ - - template = "superset/fab_overrides/list_role.html" - - def __init__(self, **kwargs: Any) -> None: - kwargs["appbuilder"] = current_app.appbuilder - super().__init__(**kwargs) - - class SupersetRoleApi(RoleApi): """ Overriding the RoleApi to be able to delete roles with permissions @@ -196,9 +174,6 @@ def pre_delete(self, item: Model) -> None: item.roles = [] -PermissionViewModelView.list_widget = SupersetSecurityListWidget -PermissionModelView.list_widget = SupersetSecurityListWidget - # Limiting routes on FAB model views PermissionViewModelView.include_route_methods = {RouteMethod.LIST} PermissionModelView.include_route_methods = {RouteMethod.LIST} diff --git a/superset/views/base.py b/superset/views/base.py index f8d44f0f9943..6ad3c4e78094 100644 --- a/superset/views/base.py +++ b/superset/views/base.py @@ -40,7 +40,6 @@ from flask_appbuilder.forms import DynamicForm from flask_appbuilder.models.sqla.filters import BaseFilter from flask_appbuilder.security.sqla.models import User -from flask_appbuilder.widgets import ListWidget from flask_babel import get_locale, gettext as __ from flask_jwt_extended.exceptions import NoAuthorizationError from flask_wtf.form import FlaskForm @@ -626,13 +625,8 @@ def get_spa_template_context( } -class SupersetListWidget(ListWidget): # pylint: disable=too-few-public-methods - template = "superset/fab_overrides/list.html" - - class SupersetModelView(ModelView): page_size = 100 - list_widget = SupersetListWidget def render_app_template(self) -> FlaskResponse: context = get_spa_template_context() From b99fc582e4e5809f97ba5a54d65f2f74055c6f18 Mon Sep 17 00:00:00 2001 From: Reynold Morel Date: Fri, 23 Jan 2026 18:15:29 -0400 Subject: [PATCH 08/13] fix(chart): implement geohash decoding (#37027) --- .../src/layers/Polygon/transformProps.test.ts | 50 +++++++++++++++++++ .../src/layers/Polygon/transformProps.ts | 11 ++++ 2 files changed, 61 insertions(+) diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/transformProps.test.ts b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/transformProps.test.ts index 50725fefe6fd..a7b7be3ab9af 100644 --- a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/transformProps.test.ts +++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/transformProps.test.ts @@ -69,6 +69,37 @@ describe('Polygon transformProps', () => { emitCrossFilters: false, }; + const mockedChartPropsWithGeoHash: Partial = { + ...mockChartProps, + rawFormData: { + line_column: 'geohash', + line_type: 'geohash', + viewport: {}, + }, + queriesData: [ + { + data: [ + { + geohash: '9q8yt', + 'sum(population)': 9800, + }, + { + geohash: '9q8yk', + 'sum(population)': 13100, + }, + { + geohash: '9q8yv', + 'sum(population)': 15600, + }, + { + geohash: '9q8yq', + 'sum(population)': 7500, + }, + ], + }, + ], + }; + test('should use constant elevation value when point_radius_fixed type is "fix"', () => { const fixProps = { ...mockChartProps, @@ -257,4 +288,23 @@ describe('Polygon transformProps', () => { expect(features).toHaveLength(1); expect(features[0]?.elevation).toBeUndefined(); }); + + test('should handle geohash decoding successfully', () => { + const props = { + ...mockedChartPropsWithGeoHash, + rawFormData: { + ...mockedChartPropsWithGeoHash.rawFormData, + point_radius_fixed: { + type: 'fix', + value: '1000', + }, + }, + }; + + const result = transformProps(props as ChartProps); + + const features = result.payload.data.features as PolygonFeature[]; + expect(features.flatMap(p => p?.polygon || [])).toHaveLength(20); // 4 geohashes x 5 corners each + expect(features[0]?.elevation).toBe(1000); + }); }); diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/transformProps.ts b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/transformProps.ts index 3c55142d39ec..bb070d92e36d 100644 --- a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/transformProps.ts +++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Polygon/transformProps.ts @@ -26,6 +26,7 @@ import { addPropertiesToFeature, } from '../transformUtils'; import { DeckPolygonFormData } from './buildQuery'; +import { decode_bbox } from 'ngeohash'; function parseElevationValue(value: string): number | undefined { const parsed = parseFloat(value); @@ -122,6 +123,16 @@ function processPolygonData( break; } case 'geohash': + polygonCoords = []; + const decoded = decode_bbox(String(rawPolygonData)); + if (decoded) { + polygonCoords.push([decoded[1], decoded[0]]); // SW (minLon, minLat) + polygonCoords.push([decoded[1], decoded[2]]); // NW (minLon, maxLat) + polygonCoords.push([decoded[3], decoded[2]]); // NE (maxLon, maxLat) + polygonCoords.push([decoded[3], decoded[0]]); // SE (maxLon, minLat) + polygonCoords.push([decoded[1], decoded[0]]); // SW (close polygon) + } + break; case 'zipcode': default: { polygonCoords = Array.isArray(rawPolygonData) ? rawPolygonData : []; From 3580dc6cad743f7d639b9f37c648bcd2a5e7e9fd Mon Sep 17 00:00:00 2001 From: Amy Li <106131209+amym-li@users.noreply.github.com> Date: Fri, 23 Jan 2026 17:30:03 -0500 Subject: [PATCH 09/13] chore(ts): Migrate Divider.jsx to Divider.tsx [SIP-36] (#36335) Co-authored-by: Mona Lisa --- .../{Divider.test.jsx => Divider.test.tsx} | 17 ++++----- .../Divider/{Divider.jsx => Divider.tsx} | 37 +++++++++---------- 2 files changed, 25 insertions(+), 29 deletions(-) rename superset-frontend/src/dashboard/components/gridComponents/Divider/{Divider.test.jsx => Divider.test.tsx} (83%) rename superset-frontend/src/dashboard/components/gridComponents/Divider/{Divider.jsx => Divider.tsx} (77%) diff --git a/superset-frontend/src/dashboard/components/gridComponents/Divider/Divider.test.jsx b/superset-frontend/src/dashboard/components/gridComponents/Divider/Divider.test.tsx similarity index 83% rename from superset-frontend/src/dashboard/components/gridComponents/Divider/Divider.test.jsx rename to superset-frontend/src/dashboard/components/gridComponents/Divider/Divider.test.tsx index e6ff744c9aeb..7657d8ea9bce 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Divider/Divider.test.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Divider/Divider.test.tsx @@ -24,11 +24,11 @@ import { DASHBOARD_GRID_TYPE, } from 'src/dashboard/util/componentTypes'; import { screen, render, userEvent } from 'spec/helpers/testing-library'; -import Divider from './Divider'; +import Divider, { DividerProps } from './Divider'; -// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks +// eslint-disable-next-line no-restricted-globals -- TODO: migrate from describe blocks describe('Divider', () => { - const props = { + const baseProps: DividerProps = { id: 'id', parentId: 'parentId', component: newComponentFactory(DIVIDER_TYPE), @@ -36,14 +36,12 @@ describe('Divider', () => { parentComponent: newComponentFactory(DASHBOARD_GRID_TYPE), index: 0, editMode: false, - handleComponentDrop() {}, - deleteComponent() {}, + handleComponentDrop: jest.fn(), + deleteComponent: (id: string, parentId: string) => {}, }; - const setup = overrideProps => - // We have to wrap provide DragDropContext for the underlying DragDroppable - // otherwise we cannot assert on DragDroppable children - render(, { + const setup = (overrideProps: Partial = {}) => + render(, { useDnd: true, }); @@ -64,7 +62,6 @@ describe('Divider', () => { expect(screen.queryByTestId('hover-menu')).not.toBeInTheDocument(); expect(screen.queryByRole('button')).not.toBeInTheDocument(); - // we cannot set props on the Divider because of the WithDragDropContext wrapper setup({ editMode: true }); expect(screen.getByTestId('hover-menu')).toBeInTheDocument(); expect(screen.getByRole('button').firstChild).toHaveAttribute( diff --git a/superset-frontend/src/dashboard/components/gridComponents/Divider/Divider.jsx b/superset-frontend/src/dashboard/components/gridComponents/Divider/Divider.tsx similarity index 77% rename from superset-frontend/src/dashboard/components/gridComponents/Divider/Divider.jsx rename to superset-frontend/src/dashboard/components/gridComponents/Divider/Divider.tsx index 7ee829f67679..a241f3dd6271 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Divider/Divider.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Divider/Divider.tsx @@ -16,31 +16,32 @@ * specific language governing permissions and limitations * under the License. */ + import { PureComponent } from 'react'; -import PropTypes from 'prop-types'; import { css, styled } from '@apache-superset/core/ui'; import { Draggable } from '../../dnd/DragDroppable'; import HoverMenu from '../../menu/HoverMenu'; import DeleteComponentButton from '../../DeleteComponentButton'; -import { componentShape } from '../../../util/propShapes'; +import type { ConnectDragSource } from 'react-dnd'; +import type { LayoutItem } from 'src/dashboard/types'; -const propTypes = { - id: PropTypes.string.isRequired, - parentId: PropTypes.string.isRequired, - component: componentShape.isRequired, - depth: PropTypes.number.isRequired, - parentComponent: componentShape.isRequired, - index: PropTypes.number.isRequired, - editMode: PropTypes.bool.isRequired, - handleComponentDrop: PropTypes.func.isRequired, - deleteComponent: PropTypes.func.isRequired, -}; +export interface DividerProps { + id: string; + parentId: string; + component: LayoutItem; + depth: number; + parentComponent: LayoutItem; + index: number; + editMode: boolean; + handleComponentDrop: (dropResult: unknown) => void; + deleteComponent: (id: string, parentId: string) => void; +} const DividerLine = styled.div` ${({ theme }) => css` width: 100%; - padding: ${theme.sizeUnit * 2}px 0; /* this is padding not margin to enable a larger mouse target */ + padding: ${theme.sizeUnit * 2}px 0; background-color: transparent; &:after { @@ -62,8 +63,8 @@ const DividerLine = styled.div` `} `; -class Divider extends PureComponent { - constructor(props) { +class Divider extends PureComponent { + constructor(props: DividerProps) { super(props); this.handleDeleteComponent = this.handleDeleteComponent.bind(this); } @@ -93,7 +94,7 @@ class Divider extends PureComponent { onDrop={handleComponentDrop} editMode={editMode} > - {({ dragSourceRef }) => ( + {({ dragSourceRef }: { dragSourceRef: ConnectDragSource }) => (
{editMode && ( @@ -108,6 +109,4 @@ class Divider extends PureComponent { } } -Divider.propTypes = propTypes; - export default Divider; From a60f8d761db579ef42dccd8115ae508fe11e32ca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 23 Jan 2026 14:31:15 -0800 Subject: [PATCH 10/13] chore(deps-dev): bump npm from 11.5.2 to 11.8.0 in /superset-frontend (#37352) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- superset-frontend/package-lock.json | 2 +- superset-frontend/packages/superset-core/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 93a8abc29bd0..fc2a0f53d5cf 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -61156,7 +61156,7 @@ "@types/react-window": "^1.8.8", "@types/tinycolor2": "*", "install": "^0.13.0", - "npm": "^11.7.0", + "npm": "^11.8.0", "typescript": "^5.0.0" }, "peerDependencies": { diff --git a/superset-frontend/packages/superset-core/package.json b/superset-frontend/packages/superset-core/package.json index dcafe39a9f0b..2c6f7df77c64 100644 --- a/superset-frontend/packages/superset-core/package.json +++ b/superset-frontend/packages/superset-core/package.json @@ -17,7 +17,7 @@ "@babel/preset-react": "^7.28.5", "@babel/preset-typescript": "^7.28.5", "install": "^0.13.0", - "npm": "^11.7.0", + "npm": "^11.8.0", "typescript": "^5.0.0", "@emotion/styled": "^11.14.1", "@types/lodash": "^4.17.23", From 3a811d680d4b46ea11708e8ad486091a625c039d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 23 Jan 2026 14:31:30 -0800 Subject: [PATCH 11/13] chore(deps): bump lodash from 4.17.21 to 4.17.23 in /superset-frontend (#37348) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- superset-frontend/package-lock.json | 31 ++++++++++++------- superset-frontend/package.json | 2 +- .../superset-ui-chart-controls/package.json | 2 +- .../packages/superset-ui-core/package.json | 2 +- .../legacy-preset-chart-deckgl/package.json | 2 +- .../legacy-preset-chart-nvd3/package.json | 2 +- .../plugin-chart-ag-grid-table/package.json | 2 +- .../plugin-chart-cartodiagram/package.json | 2 +- .../plugins/plugin-chart-echarts/package.json | 2 +- .../plugins/plugin-chart-table/package.json | 2 +- 10 files changed, 28 insertions(+), 21 deletions(-) diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index fc2a0f53d5cf..0908ecd85686 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -87,7 +87,7 @@ "js-yaml-loader": "^1.2.2", "json-bigint": "^1.0.0", "json-stringify-pretty-compact": "^2.0.0", - "lodash": "^4.17.21", + "lodash": "^4.17.23", "mapbox-gl": "^3.18.1", "markdown-to-jsx": "^9.6.0", "match-sorter": "^6.3.4", @@ -739,6 +739,13 @@ "node": ">=18.0.0" } }, + "node_modules/@applitools/eyes-storybook/node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, "node_modules/@applitools/eyes/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -42856,9 +42863,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, "node_modules/lodash-es": { @@ -63864,7 +63871,7 @@ "@apache-superset/core": "*", "@react-icons/all-files": "^4.1.0", "@types/react": "*", - "lodash": "^4.17.21" + "lodash": "^4.17.23" }, "peerDependencies": { "@ant-design/icons": "^5.2.6", @@ -63910,7 +63917,7 @@ "fetch-retry": "^6.0.0", "handlebars": "^4.7.8", "jed": "^1.1.1", - "lodash": "^4.17.21", + "lodash": "^4.17.23", "math-expression-evaluator": "^2.0.7", "pretty-ms": "^9.3.0", "re-resizable": "^6.11.2", @@ -66005,7 +66012,7 @@ "d3-color": "^1.4.1", "d3-scale": "^3.0.0", "handlebars": "^4.7.8", - "lodash": "^4.17.21", + "lodash": "^4.17.23", "mousetrap": "^1.6.5", "ngeohash": "^0.6.3", "prop-types": "^15.8.1", @@ -66271,7 +66278,7 @@ "d3-tip": "^0.9.1", "dompurify": "^3.3.1", "fast-safe-stringify": "^2.1.1", - "lodash": "^4.17.21", + "lodash": "^4.17.23", "nvd3-fork": "^2.0.5", "prop-types": "^15.8.1", "urijs": "^1.19.11" @@ -66303,7 +66310,7 @@ "@types/react-table": "^7.7.20", "classnames": "^2.5.1", "d3-array": "^3.2.4", - "lodash": "^4.17.21", + "lodash": "^4.17.23", "memoize-one": "^5.2.1", "react-table": "^7.8.0", "regenerator-runtime": "^0.14.1", @@ -66350,7 +66357,7 @@ "dependencies": { "@types/geojson": "^7946.0.10", "geojson": "^0.5.0", - "lodash": "^4.17.21" + "lodash": "^4.17.23" }, "peerDependencies": { "@ant-design/icons": "^5.2.6", @@ -66377,7 +66384,7 @@ "dependencies": { "@types/react-redux": "^7.1.34", "d3-array": "^3.2.4", - "lodash": "^4.17.21" + "lodash": "^4.17.23" }, "peerDependencies": { "@apache-superset/core": "*", @@ -66498,7 +66505,7 @@ "@types/react-table": "^7.7.20", "classnames": "^2.5.1", "d3-array": "^3.2.4", - "lodash": "^4.17.21", + "lodash": "^4.17.23", "memoize-one": "^5.2.1", "react-table": "^7.8.0", "regenerator-runtime": "^0.14.1", diff --git a/superset-frontend/package.json b/superset-frontend/package.json index 6178e08d723f..d5ab327cc9a7 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -168,7 +168,7 @@ "js-yaml-loader": "^1.2.2", "json-bigint": "^1.0.0", "json-stringify-pretty-compact": "^2.0.0", - "lodash": "^4.17.21", + "lodash": "^4.17.23", "mapbox-gl": "^3.18.1", "markdown-to-jsx": "^9.6.0", "match-sorter": "^6.3.4", diff --git a/superset-frontend/packages/superset-ui-chart-controls/package.json b/superset-frontend/packages/superset-ui-chart-controls/package.json index 93d06daaaa27..061eba7bd4aa 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/package.json +++ b/superset-frontend/packages/superset-ui-chart-controls/package.json @@ -27,7 +27,7 @@ "@apache-superset/core": "*", "@react-icons/all-files": "^4.1.0", "@types/react": "*", - "lodash": "^4.17.21" + "lodash": "^4.17.23" }, "peerDependencies": { "@ant-design/icons": "^5.2.6", diff --git a/superset-frontend/packages/superset-ui-core/package.json b/superset-frontend/packages/superset-ui-core/package.json index c905abb940fa..91dca86f9615 100644 --- a/superset-frontend/packages/superset-ui-core/package.json +++ b/superset-frontend/packages/superset-ui-core/package.json @@ -45,7 +45,7 @@ "fetch-retry": "^6.0.0", "handlebars": "^4.7.8", "jed": "^1.1.1", - "lodash": "^4.17.21", + "lodash": "^4.17.23", "math-expression-evaluator": "^2.0.7", "pretty-ms": "^9.3.0", "re-resizable": "^6.11.2", diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/package.json b/superset-frontend/plugins/legacy-preset-chart-deckgl/package.json index d5abdfa75ca1..69ba848b0a2e 100644 --- a/superset-frontend/plugins/legacy-preset-chart-deckgl/package.json +++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/package.json @@ -46,7 +46,7 @@ "d3-color": "^1.4.1", "d3-scale": "^3.0.0", "handlebars": "^4.7.8", - "lodash": "^4.17.21", + "lodash": "^4.17.23", "mousetrap": "^1.6.5", "ngeohash": "^0.6.3", "prop-types": "^15.8.1", diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/package.json b/superset-frontend/plugins/legacy-preset-chart-nvd3/package.json index 57ca58750ef3..640a338a831b 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/package.json +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/package.json @@ -32,7 +32,7 @@ "d3": "^3.5.17", "d3-tip": "^0.9.1", "fast-safe-stringify": "^2.1.1", - "lodash": "^4.17.21", + "lodash": "^4.17.23", "nvd3-fork": "^2.0.5", "dompurify": "^3.3.1", "prop-types": "^15.8.1", diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/package.json b/superset-frontend/plugins/plugin-chart-ag-grid-table/package.json index c6d4720cabcd..6d23f8bd2411 100644 --- a/superset-frontend/plugins/plugin-chart-ag-grid-table/package.json +++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/package.json @@ -29,7 +29,7 @@ "@types/react-table": "^7.7.20", "classnames": "^2.5.1", "d3-array": "^3.2.4", - "lodash": "^4.17.21", + "lodash": "^4.17.23", "memoize-one": "^5.2.1", "react-table": "^7.8.0", "regenerator-runtime": "^0.14.1", diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/package.json b/superset-frontend/plugins/plugin-chart-cartodiagram/package.json index 3fde837abf61..abca2cc0ab0e 100644 --- a/superset-frontend/plugins/plugin-chart-cartodiagram/package.json +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/package.json @@ -31,7 +31,7 @@ "dependencies": { "@types/geojson": "^7946.0.10", "geojson": "^0.5.0", - "lodash": "^4.17.21" + "lodash": "^4.17.23" }, "peerDependencies": { "@ant-design/icons": "^5.2.6", diff --git a/superset-frontend/plugins/plugin-chart-echarts/package.json b/superset-frontend/plugins/plugin-chart-echarts/package.json index ae346f4e44f9..3d8de503a249 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/package.json +++ b/superset-frontend/plugins/plugin-chart-echarts/package.json @@ -26,7 +26,7 @@ "dependencies": { "@types/react-redux": "^7.1.34", "d3-array": "^3.2.4", - "lodash": "^4.17.21" + "lodash": "^4.17.23" }, "peerDependencies": { "@apache-superset/core": "*", diff --git a/superset-frontend/plugins/plugin-chart-table/package.json b/superset-frontend/plugins/plugin-chart-table/package.json index d439f0ec1505..27b073ec388f 100644 --- a/superset-frontend/plugins/plugin-chart-table/package.json +++ b/superset-frontend/plugins/plugin-chart-table/package.json @@ -29,7 +29,7 @@ "@types/react-table": "^7.7.20", "classnames": "^2.5.1", "d3-array": "^3.2.4", - "lodash": "^4.17.21", + "lodash": "^4.17.23", "memoize-one": "^5.2.1", "react-table": "^7.8.0", "regenerator-runtime": "^0.14.1", From 3f37cdbf9c6443a5306f21ca94dfe0c17c925378 Mon Sep 17 00:00:00 2001 From: isaac-jaynes-imperva Date: Fri, 23 Jan 2026 14:34:52 -0800 Subject: [PATCH 12/13] fix(database): include `configuration_method` in the DB export/import flow (#36958) Co-authored-by: codeant-ai-for-open-source[bot] <244253245+codeant-ai-for-open-source[bot]@users.noreply.github.com> --- superset/databases/api.py | 1 + superset/databases/schemas.py | 7 + superset/models/core.py | 1 + .../integration_tests/databases/api_tests.py | 1 + .../databases/commands_tests.py | 1 + tests/unit_tests/databases/api_test.py | 149 ++++++++++++++++++ .../datasets/commands/export_test.py | 1 + 7 files changed, 161 insertions(+) diff --git a/superset/databases/api.py b/superset/databases/api.py index b0f29c5247a2..e9178d1f24eb 100644 --- a/superset/databases/api.py +++ b/superset/databases/api.py @@ -218,6 +218,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi): "changed_by.last_name", "created_by.first_name", "created_by.last_name", + "configuration_method", "database_name", "explore_database_id", "expose_in_sqllab", diff --git a/superset/databases/schemas.py b/superset/databases/schemas.py index 105496efa4c6..199da14b63c8 100644 --- a/superset/databases/schemas.py +++ b/superset/databases/schemas.py @@ -888,6 +888,13 @@ def fix_allow_csv_upload( is_managed_externally = fields.Boolean(allow_none=True, dump_default=False) external_url = fields.String(allow_none=True) ssh_tunnel = fields.Nested(DatabaseSSHTunnel, allow_none=True) + configuration_method = fields.Enum( + ConfigurationMethod, + by_value=True, + required=False, + allow_none=True, + load_default=ConfigurationMethod.SQLALCHEMY_FORM, + ) @validates_schema def validate_password(self, data: dict[str, Any], **kwargs: Any) -> None: diff --git a/superset/models/core.py b/superset/models/core.py index d13c14b65ab6..2de0a053913b 100755 --- a/superset/models/core.py +++ b/superset/models/core.py @@ -197,6 +197,7 @@ class Database(CoreDatabase, AuditMixinNullable, ImportExportMixin): # pylint: "allow_file_upload", "extra", "impersonate_user", + "configuration_method", ] extra_import_fields = [ "password", diff --git a/tests/integration_tests/databases/api_tests.py b/tests/integration_tests/databases/api_tests.py index 94b24896a592..f77a47c8a334 100644 --- a/tests/integration_tests/databases/api_tests.py +++ b/tests/integration_tests/databases/api_tests.py @@ -189,6 +189,7 @@ def test_get_items(self): "changed_by", "changed_on", "changed_on_delta_humanized", + "configuration_method", "created_by", "database_name", "disable_data_preview", diff --git a/tests/integration_tests/databases/commands_tests.py b/tests/integration_tests/databases/commands_tests.py index 529e4807e70d..27c1ce565428 100644 --- a/tests/integration_tests/databases/commands_tests.py +++ b/tests/integration_tests/databases/commands_tests.py @@ -371,6 +371,7 @@ def test_export_database_command_key_order(self, mock_g): "allow_csv_upload", "extra", "impersonate_user", + "configuration_method", "uuid", "version", ] diff --git a/tests/unit_tests/databases/api_test.py b/tests/unit_tests/databases/api_test.py index 0785d762e50e..7b77f5099d96 100644 --- a/tests/unit_tests/databases/api_test.py +++ b/tests/unit_tests/databases/api_test.py @@ -2274,3 +2274,152 @@ def test_schemas_with_oauth2( } ] } + + +def test_export_includes_configuration_method( + mocker: MockerFixture, client: Any, full_api_access: None +) -> None: + """ + Test that exporting a database + includes the 'configuration_method' field in the YAML. + """ + import zipfile + + import prison + + from superset.models.core import Database + + # Create a database with a non-default configuration_method + db_obj = Database( + database_name="export_test_db", + sqlalchemy_uri="bigquery://gcp-project-id/", + configuration_method="dynamic_form", + uuid=UUID("12345678-1234-5678-1234-567812345678"), + ) + db.session.add(db_obj) + db.session.commit() + + rison_ids = prison.dumps([db_obj.id]) + response = client.get(f"/api/v1/database/export/?q={rison_ids}") + assert response.status_code == 200 + + # Read the zip file from the response + buf = BytesIO(response.data) + with zipfile.ZipFile(buf) as zf: + # Find the database yaml file + db_yaml_path = None + for name in zf.namelist(): + if ( + name.endswith(".yaml") + and name.startswith("database_export_") + and "/databases/" in name + ): + db_yaml_path = name + break + assert db_yaml_path, "Database YAML not found in export zip" + with zf.open(db_yaml_path) as f: + db_yaml = yaml.safe_load(f.read()) + # Assert configuration_method is present and correct + assert "configuration_method" in db_yaml + assert db_yaml["configuration_method"] == "dynamic_form" + + +def test_import_includes_configuration_method( + mocker: MockerFixture, + client: Any, + full_api_access: None, +) -> None: + """ + Test that importing a database YAML with configuration_method + sets the value on the imported DB connection. + """ + from io import BytesIO + from unittest.mock import patch + + import yaml + from flask import g, has_app_context, has_request_context + + from superset import db, security_manager + from superset.databases.api import DatabaseRestApi + from superset.models.core import Database + + DatabaseRestApi.datamodel._session = db.session + Database.metadata.create_all(db.session.get_bind()) + + def find_by_id_side_effect(db_id): + return db.session.query(Database).filter_by(id=db_id).first() + + DatabaseDAO = mocker.patch("superset.databases.api.DatabaseDAO") # noqa: N806 + DatabaseDAO.find_by_id.side_effect = find_by_id_side_effect + + metadata = { + "version": "1.0.0", + "type": "Database", + "timestamp": "2025-12-08T18:06:31.356738+00:00", + } + db_yaml = { + "database_name": "Test_Import_Configuration_Method", + "sqlalchemy_uri": "bigquery://gcp-project-id/", + "cache_timeout": 0, + "expose_in_sqllab": True, + "allow_run_async": False, + "allow_ctas": False, + "allow_cvas": False, + "allow_dml": False, + "allow_csv_upload": False, + "extra": {"allows_virtual_table_explore": True}, + "impersonate_user": False, + "uuid": "87654321-4321-8765-4321-876543218765", + "configuration_method": "dynamic_form", + "version": "1.0.0", + } + contents = { + "metadata.yaml": yaml.safe_dump(metadata), + "databases/test.yaml": yaml.safe_dump(db_yaml), + } + + with ( + patch("superset.databases.api.is_zipfile", return_value=True), + patch("superset.databases.api.ZipFile"), + patch("superset.databases.api.get_contents_from_bundle", return_value=contents), + ): + form_data = {"formData": (BytesIO(b"test"), "test.zip")} + response = client.post( + "/api/v1/database/import/", + data=form_data, + content_type="multipart/form-data", + ) + db.session.commit() + db.session.remove() + assert response.status_code == 200, response.data + + db_obj = ( + db.session.query(Database) + .filter_by(database_name="Test_Import_Configuration_Method") + .first() + ) + assert db_obj is not None, "Database not found in SQLAlchemy session after import" + assert hasattr(db_obj, "configuration_method"), ( + "'configuration_method' not found on model" + ) + assert db_obj.configuration_method == "dynamic_form", ( + "Expected configuration_method 'dynamic_form', got " + f"{db_obj.configuration_method}" + ) + + user = None + if has_request_context() or has_app_context(): + user = getattr(g, "user", None) + if user and getattr(user, "is_authenticated", False) and hasattr(user, "id"): + db_obj.created_by = security_manager.get_user_by_id(user.id) + db.session.commit() + get_resp = client.get( + "/api/v1/database/?q=(filters:!((col:database_name,opr:eq,value:'Test_Import_Configuration_Method')))" + ) + result = get_resp.json["result"] + assert result, "No database returned from API after import." + db_obj_api = result[0] + assert "configuration_method" in db_obj_api, ( + f"'configuration_method' not found in database list response: {db_obj_api}" + ) + assert db_obj_api["configuration_method"] == "dynamic_form" diff --git a/tests/unit_tests/datasets/commands/export_test.py b/tests/unit_tests/datasets/commands/export_test.py index d2bb3a66ccf5..a449b764084d 100644 --- a/tests/unit_tests/datasets/commands/export_test.py +++ b/tests/unit_tests/datasets/commands/export_test.py @@ -298,6 +298,7 @@ def test_export(session: Session) -> None: metadata_cache_timeout: {{}} schemas_allowed_for_file_upload: [] impersonate_user: false +configuration_method: sqlalchemy_form uuid: {database.uuid} version: 1.0.0 """, From 319a131ec91a6dd93ff2a548206f177c73cc85f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20L=C3=B3pez?= Date: Fri, 23 Jan 2026 20:08:16 -0300 Subject: [PATCH 13/13] fix(charts): missing globalOpacity prop with mapbox (#37168) --- .../src/ScatterPlotGlowOverlay.jsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/superset-frontend/plugins/legacy-plugin-chart-map-box/src/ScatterPlotGlowOverlay.jsx b/superset-frontend/plugins/legacy-plugin-chart-map-box/src/ScatterPlotGlowOverlay.jsx index 011c91ed9cd1..739204c134ae 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-map-box/src/ScatterPlotGlowOverlay.jsx +++ b/superset-frontend/plugins/legacy-plugin-chart-map-box/src/ScatterPlotGlowOverlay.jsx @@ -29,6 +29,7 @@ const propTypes = { aggregation: PropTypes.string, compositeOperation: PropTypes.string, dotRadius: PropTypes.number, + globalOpacity: PropTypes.number, lngLatAccessor: PropTypes.func, locations: PropTypes.arrayOf(PropTypes.object).isRequired, pointRadiusUnit: PropTypes.string, @@ -121,6 +122,7 @@ class ScatterPlotGlowOverlay extends PureComponent { aggregation, compositeOperation, dotRadius, + globalOpacity, lngLatAccessor, locations, pointRadiusUnit, @@ -180,7 +182,7 @@ class ScatterPlotGlowOverlay extends PureComponent { gradient.addColorStop( 1, - `rgba(${rgb[1]}, ${rgb[2]}, ${rgb[3]}, 0.8)`, + `rgba(${rgb[1]}, ${rgb[2]}, ${rgb[3]}, ${0.8 * globalOpacity})`, ); gradient.addColorStop( 0, @@ -251,7 +253,7 @@ class ScatterPlotGlowOverlay extends PureComponent { 0, Math.PI * 2, ); - ctx.fillStyle = `rgb(${rgb[1]}, ${rgb[2]}, ${rgb[3]})`; + ctx.fillStyle = `rgba(${rgb[1]}, ${rgb[2]}, ${rgb[3]}, ${globalOpacity})`; ctx.fill(); if (pointLabel !== undefined) {