Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/generate-FOSSA-report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
persist-credentials: false
submodules: recursive
- name: Setup Java
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
with:
distribution: "temurin"
java-version: "11"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/license-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
persist-credentials: false
submodules: recursive
- name: Setup Java
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
with:
distribution: "temurin"
java-version: "11"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/superset-docs-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ jobs:
node-version-file: "./docs/.nvmrc"
- name: Setup Python
uses: ./.github/actions/setup-backend/
- uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
- uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
with:
distribution: "zulu"
java-version: "21"
Expand Down
2 changes: 1 addition & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
"@superset-ui/core": "^0.20.4",
"@swc/core": "^1.16.1",
"antd": "^6.6.1",
"baseline-browser-mapping": "^2.11.17",
"baseline-browser-mapping": "^2.11.18",
"caniuse-lite": "^1.0.30001809",
"docusaurus-plugin-openapi-docs": "^5.2.0",
"docusaurus-theme-openapi-docs": "^5.2.0",
Expand Down
8 changes: 4 additions & 4 deletions docs/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6471,10 +6471,10 @@ base64-js@^1.3.1, base64-js@^1.5.1:
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==

baseline-browser-mapping@^2.10.38, baseline-browser-mapping@^2.11.17, baseline-browser-mapping@^2.9.19:
version "2.11.17"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.17.tgz#a168205490077c5d7c542f1610016af4ade8f875"
integrity sha512-KAUDn1OSS0fmPlGO+NOUMRcOQ/b/shUBH3OgkG73mPgdf+JD/BQ6fHboGxNOxnUmlwcq+lLq3dTkayRPuSfXwg==
baseline-browser-mapping@^2.10.38, baseline-browser-mapping@^2.11.18, baseline-browser-mapping@^2.9.19:
version "2.11.18"
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.11.18.tgz#49701f6ab6c58ccafb6d8e1ab2767574d9f0ba73"
integrity sha512-1iEmLEYSiE1SeBoAfPo/Mnx3PzfzHUkDK61ASkCpuk3YXugYLH5DYK1SzqV55F8FMI6s0F+/tCP7Polz1QRjxw==

batch@0.6.1:
version "0.6.1"
Expand Down
24 changes: 12 additions & 12 deletions superset-frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions superset-frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@
"babel-loader": "^10.1.1",
"babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
"baseline-browser-mapping": "^2.11.17",
"baseline-browser-mapping": "^2.11.18",
"cheerio": "1.2.0",
"concurrently": "^10.0.5",
"copy-webpack-plugin": "^14.0.0",
Expand Down Expand Up @@ -354,7 +354,7 @@
"unzipper": "^0.12.5",
"wait-on": "^9.1.0",
"webpack": "^5.109.2",
"webpack-bundle-analyzer": "^5.3.1",
"webpack-bundle-analyzer": "^5.3.2",
"webpack-cli": "^7.0.3",
"webpack-dev-server": "^5.2.5",
"webpack-manifest-plugin": "^6.0.1",
Expand Down
2 changes: 1 addition & 1 deletion superset-frontend/packages/generator-superset/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"dependencies": {
"chalk": "^6.0.0",
"lodash-es": "^4.18.1",
"yeoman-generator": "^8.2.2",
"yeoman-generator": "^8.3.0",
"yosay": "^3.0.0"
},
"devDependencies": {
Expand Down
7 changes: 5 additions & 2 deletions superset/db_engine_specs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,14 @@ def get_available_engine_specs() -> dict[type[BaseEngineSpec], set[str]]: # noq
issubclass(dialect, DefaultDialect)
and hasattr(dialect, "driver")
# adodbapi dialect is removed in SQLA 1.4 and doesn't implement the
# `dbapi` method, hence needs to be ignored to avoid logging a warning
# DBAPI import method, hence needs to be ignored to avoid a warning
and dialect.driver != "adodbapi"
):
try:
dialect.dbapi()
if hasattr(dialect, "import_dbapi"):
dialect.import_dbapi()
else:
dialect.dbapi()
except ModuleNotFoundError:
continue
except Exception as ex: # pylint: disable=broad-except
Expand Down
9 changes: 9 additions & 0 deletions superset/mcp_service/dashboard/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,15 @@ class GetDashboardInfoRequest(MetadataCacheControl):
"the shared active-tab and filter context; no identifier is required."
),
)
filter_state: dict[str, Any] | None = Field(
default=None,
description=(
"Active filters supplied directly rather than via a permalink, so the "
"tool can describe the dashboard as the user currently views it, "
'filtered. Shape: {"applied_filters": [{"col", "op", "val"}]}. Ignored '
"when permalink_key is provided."
),
)
select_columns: Annotated[
List[str],
Field(
Expand Down
32 changes: 23 additions & 9 deletions superset/mcp_service/dashboard/tool/get_dashboard_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,25 +40,29 @@
dashboard_serializer,
DashboardError,
DashboardInfo,
DEFAULT_GET_DASHBOARD_INFO_COLUMNS,
GetDashboardInfoRequest,
redact_filter_state_data_model_metadata,
)
from superset.mcp_service.mcp_core import ModelGetInfoCore
from superset.mcp_service.privacy import user_can_view_data_model_metadata

logger = logging.getLogger(__name__)


def _apply_permalink_state(
result: DashboardInfo,
permalink_key: str,
permalink_key: str | None,
permalink_state: dict[str, object],
is_permalink: bool = True,
) -> DashboardInfo:
"""Attach permalink fields without changing their stored values."""
"""Attach the filter state without changing its stored values.
is_permalink is False when the state was supplied directly, not resolved
from a permalink."""
return result.model_copy(
update={
"permalink_key": permalink_key,
"filter_state": permalink_state,
"is_permalink_state": True,
"is_permalink_state": is_permalink,
}
)

Expand Down Expand Up @@ -214,6 +218,15 @@ async def get_dashboard_info(
"permalink_key provided but no permalink found. "
"The permalink may have expired or is invalid."
)
elif request.filter_state is not None:
# Filter context supplied directly (no permalink), e.g. embedded.
await ctx.info("Applying caller-supplied filter_state")
filter_state = request.filter_state
if not user_can_view_data_model_metadata():
filter_state = redact_filter_state_data_model_metadata(filter_state)
result = _apply_permalink_state(
result, None, filter_state, is_permalink=False
)

await ctx.info(
"Dashboard information retrieved successfully: id=%s, title=%s, "
Expand All @@ -226,12 +239,13 @@ async def get_dashboard_info(
result.is_permalink_state,
)
)
# When permalink_key is supplied and the caller did not explicitly
# override select_columns, ensure filter_state is present so the
# caller gets the data they came for.
# Include filter_state by default when present, but honor an explicit
# select_columns projection (model_fields_set = caller chose it).
effective_select_columns = list(request.select_columns)
if result.is_permalink_state and effective_select_columns == list(
DEFAULT_GET_DASHBOARD_INFO_COLUMNS
if (
result.filter_state is not None
and "select_columns" not in request.model_fields_set
and "filter_state" not in effective_select_columns
):
effective_select_columns.append("filter_state")

Expand Down
35 changes: 35 additions & 0 deletions tests/unit_tests/db_engine_specs/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,41 @@ class ValidDialect(DefaultDialect):
assert available[SqliteEngineSpec] == {"valid_driver"}


def test_get_available_engine_specs_supports_sqlalchemy_2_native_dialect(
mocker: MockerFixture,
) -> None:
"""A native SQLAlchemy 2 dialect is discovered through import_dbapi()."""
import sqlalchemy.dialects

from superset.db_engine_specs.mysql import MySQLEngineSpec

class ValidDialect(DefaultDialect):
driver = "mysqldb"

@classmethod
def import_dbapi(cls) -> object:
return object()

mocker.patch.object(sqlalchemy.dialects, "__all__", ["mysql"])
mocker.patch.object(
sqlalchemy.dialects.registry,
"load",
return_value=ValidDialect,
)
mocker.patch(
"superset.db_engine_specs.load_engine_specs",
return_value=iter([MySQLEngineSpec]),
)
mocker.patch(
"superset.db_engine_specs.entry_points",
return_value=[],
)

available = get_available_engine_specs()

assert available[MySQLEngineSpec] == {"mysqldb"}


@pytest.mark.parametrize(
"app",
[{"DBS_AVAILABLE_DENYLIST": {"databricks": {"pyhive", "pyodbc"}}}],
Expand Down
11 changes: 11 additions & 0 deletions tests/unit_tests/mcp_service/dashboard/test_dashboard_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -976,6 +976,17 @@ def test_get_dashboard_info_select_columns_columns_alias(self) -> None:
)
assert req.select_columns == ["id", "dashboard_title"]

def test_get_dashboard_info_accepts_filter_state(self) -> None:
applied = {"applied_filters": [{"col": "gender", "op": "IN", "val": ["F"]}]}
req = GetDashboardInfoRequest.model_validate(
{"identifier": 42, "filter_state": applied}
)
assert req.filter_state == applied

def test_get_dashboard_info_filter_state_defaults_none(self) -> None:
req = GetDashboardInfoRequest.model_validate({"identifier": 42})
assert req.filter_state is None

@pytest.mark.parametrize(
"payload",
[
Expand Down
Loading
Loading