From 8d95b8f4fbb669216ebe43b5fd2a6da356bafc56 Mon Sep 17 00:00:00 2001 From: Amin Ghadersohi Date: Fri, 31 Jul 2026 21:39:03 +0000 Subject: [PATCH 1/2] fix(mcp): expose table column formatting --- superset/mcp_service/chart/chart_utils.py | 5 +++ superset/mcp_service/chart/schemas.py | 39 +++++++++++++++++++ .../mcp_service/chart/tool/update_chart.py | 7 ++++ .../chart/tool/update_chart_preview.py | 4 ++ .../mcp_service/chart/test_new_chart_types.py | 35 +++++++++++++++++ .../chart/tool/test_get_chart_type_schema.py | 7 +++- .../chart/tool/test_update_chart.py | 20 ++++++++++ .../chart/tool/test_update_chart_preview.py | 6 ++- 8 files changed, 121 insertions(+), 2 deletions(-) diff --git a/superset/mcp_service/chart/chart_utils.py b/superset/mcp_service/chart/chart_utils.py index d02e9aeb9ce0..c5eb72b121e4 100644 --- a/superset/mcp_service/chart/chart_utils.py +++ b/superset/mcp_service/chart/chart_utils.py @@ -567,6 +567,11 @@ def map_table_config(config: TableChartConfig) -> Dict[str, Any]: form_data["row_limit"] = config.row_limit add_color_scheme(form_data, config.color_scheme) + if config.column_config is not None: + form_data["column_config"] = { + label: column.model_dump(by_alias=True, exclude_none=True) + for label, column in config.column_config.items() + } return form_data diff --git a/superset/mcp_service/chart/schemas.py b/superset/mcp_service/chart/schemas.py index f58da9460880..c1fd8bc24ec0 100644 --- a/superset/mcp_service/chart/schemas.py +++ b/superset/mcp_service/chart/schemas.py @@ -1540,6 +1540,33 @@ def validate_metric_aggregate(self) -> Self: return self +class TableColumnConfig(UnknownFieldCheckMixin): + """Display formatting supported by the MCP table-chart schema.""" + + model_config = ConfigDict(extra="ignore", populate_by_name=True) + + column_width: int | None = Field( + None, + alias="columnWidth", + description="Minimum column width in pixels.", + ge=0, + ) + d3_number_format: str | None = Field( + None, + alias="d3NumberFormat", + description="D3 number format, for example ',.2f', '$,.2f', or '.1%'.", + min_length=1, + max_length=100, + ) + d3_time_format: str | None = Field( + None, + alias="d3TimeFormat", + description="D3 time format, for example '%Y-%m-%d' or '%b %d, %Y'.", + min_length=1, + max_length=100, + ) + + class TableChartConfig(UnknownFieldCheckMixin): model_config = ConfigDict(extra="ignore", populate_by_name=True) @@ -1586,6 +1613,18 @@ class TableChartConfig(UnknownFieldCheckMixin): ), max_length=100, ) + column_config: dict[str, "TableColumnConfig"] | None = Field( + None, + description=( + "Per-column display settings, keyed by the result column label " + "(for a raw column this is usually its column name; for a metric, use " + "its label). Use columnWidth for minimum width in pixels, " + "d3NumberFormat for D3 number formats such as ',.2f' or '.1%', and " + "d3TimeFormat for D3 time formats such as '%Y-%m-%d'. Example: " + "{'Total Sales': {'columnWidth': 120, 'd3NumberFormat': '$,.2f'}, " + "'Order Date': {'d3TimeFormat': '%Y-%m-%d'}}." + ), + ) @model_validator(mode="after") def reject_sql_expression_in_raw_mode(self) -> "TableChartConfig": diff --git a/superset/mcp_service/chart/tool/update_chart.py b/superset/mcp_service/chart/tool/update_chart.py index b18e8e10f8bc..121293d3cd30 100644 --- a/superset/mcp_service/chart/tool/update_chart.py +++ b/superset/mcp_service/chart/tool/update_chart.py @@ -117,6 +117,13 @@ def _build_update_payload( parsed_config, dataset_id=effective_dataset_id ) new_form_data.pop("_mcp_warnings", None) + if getattr(parsed_config, "column_config", None) is None: + try: + existing_form_data = json.loads(chart.params) if chart.params else {} + except (ValueError, TypeError): + existing_form_data = {} + if existing_column_config := existing_form_data.get("column_config"): + new_form_data["column_config"] = existing_column_config chart_name = ( request.chart_name diff --git a/superset/mcp_service/chart/tool/update_chart_preview.py b/superset/mcp_service/chart/tool/update_chart_preview.py index 78f11f066ad0..d394164bd735 100644 --- a/superset/mcp_service/chart/tool/update_chart_preview.py +++ b/superset/mcp_service/chart/tool/update_chart_preview.py @@ -186,6 +186,10 @@ def update_chart_preview( # noqa: C901 old_adhoc_filters = previous_form_data.get("adhoc_filters") if old_adhoc_filters: new_form_data["adhoc_filters"] = old_adhoc_filters + if getattr(config, "column_config", None) is None and previous_form_data: + old_column_config = previous_form_data.get("column_config") + if old_column_config: + new_form_data["column_config"] = old_column_config # Tier-1 schema validation against the dataset (no DB roundtrip). # Runs AFTER the filter merge so filter columns are also validated. diff --git a/tests/unit_tests/mcp_service/chart/test_new_chart_types.py b/tests/unit_tests/mcp_service/chart/test_new_chart_types.py index 48b1631568a9..8b7aa87cf7ad 100644 --- a/tests/unit_tests/mcp_service/chart/test_new_chart_types.py +++ b/tests/unit_tests/mcp_service/chart/test_new_chart_types.py @@ -1240,6 +1240,41 @@ def test_color_scheme_omitted_when_unset(self) -> None: assert "color_scheme" not in result + def test_column_config_in_form_data(self) -> None: + config = TableChartConfig.model_validate( + { + "chart_type": "table", + "columns": [ + {"name": "order_date"}, + {"name": "revenue", "aggregate": "SUM", "label": "Revenue"}, + ], + "column_config": { + "order_date": { + "columnWidth": 120, + "d3TimeFormat": "%Y-%m-%d", + }, + "Revenue": {"d3NumberFormat": "$,.2f"}, + }, + } + ) + + result = map_table_config(config) + + assert result["column_config"] == { + "order_date": {"columnWidth": 120, "d3TimeFormat": "%Y-%m-%d"}, + "Revenue": {"d3NumberFormat": "$,.2f"}, + } + + def test_column_config_rejects_unknown_setting(self) -> None: + with pytest.raises(ValidationError, match="Unknown field 'width'"): + TableChartConfig.model_validate( + { + "chart_type": "table", + "columns": [{"name": "product"}], + "column_config": {"product": {"width": 120}}, + } + ) + class TestCurrencyFormatModel: """CurrencyFormat schema validation.""" diff --git a/tests/unit_tests/mcp_service/chart/tool/test_get_chart_type_schema.py b/tests/unit_tests/mcp_service/chart/tool/test_get_chart_type_schema.py index e90cbef3519d..1778f8c8ad9f 100644 --- a/tests/unit_tests/mcp_service/chart/tool/test_get_chart_type_schema.py +++ b/tests/unit_tests/mcp_service/chart/tool/test_get_chart_type_schema.py @@ -44,10 +44,15 @@ def test_xy_schema_has_expected_fields(self) -> None: assert "y" in props assert "kind" in props - def test_table_schema_has_columns(self) -> None: + def test_table_schema_has_columns_and_column_config(self) -> None: result = _call_schema("table") props = result["schema"]["properties"] assert "columns" in props + assert "column_config" in props + description = props["column_config"]["description"] + assert "columnWidth" in description + assert "d3NumberFormat" in description + assert "d3TimeFormat" in description def test_pie_schema_has_dimension_metric(self) -> None: result = _call_schema("pie") diff --git a/tests/unit_tests/mcp_service/chart/tool/test_update_chart.py b/tests/unit_tests/mcp_service/chart/tool/test_update_chart.py index a4d1da7b2693..807fe2a7ad02 100644 --- a/tests/unit_tests/mcp_service/chart/tool/test_update_chart.py +++ b/tests/unit_tests/mcp_service/chart/tool/test_update_chart.py @@ -114,6 +114,26 @@ async def test_update_chart_with_chart_name(self): ) assert request2.chart_name == "Updated Sales Report" + def test_unrelated_config_update_preserves_existing_column_config(self) -> None: + chart = Mock( + datasource_id=7, + params='{"column_config":{"Revenue":{"columnWidth":160}}}', + slice_name="Revenue table", + ) + request = UpdateChartRequest( + identifier=123, + config=TableChartConfig( + chart_type="table", + columns=[ColumnRef(name="revenue", aggregate="SUM")], + row_limit=500, + ), + ) + + payload = _build_update_payload(request, chart, request.config) + + assert isinstance(payload, dict) + assert '"column_config": {"Revenue": {"columnWidth": 160}}' in payload["params"] + @pytest.mark.asyncio async def test_update_chart_preview_formats(self): """Test preview_formats options in update request.""" diff --git a/tests/unit_tests/mcp_service/chart/tool/test_update_chart_preview.py b/tests/unit_tests/mcp_service/chart/tool/test_update_chart_preview.py index fe5c88b9d91b..6e126dfe13e8 100644 --- a/tests/unit_tests/mcp_service/chart/tool/test_update_chart_preview.py +++ b/tests/unit_tests/mcp_service/chart/tool/test_update_chart_preview.py @@ -689,7 +689,8 @@ async def test_preserves_previous_adhoc_filters_without_warning( } ] mock_get_previous_form_data.return_value = { - "adhoc_filters": cached_adhoc_filters + "adhoc_filters": cached_adhoc_filters, + "column_config": {"Sales": {"d3NumberFormat": "$,.2f"}}, } mock_generate_explore_link.return_value = ( "http://localhost:8088/explore/?form_data_key=new_preview_key" @@ -718,6 +719,9 @@ async def test_preserves_previous_adhoc_filters_without_warning( generated_form_data = mock_generate_explore_link.call_args.args[1] assert generated_form_data["adhoc_filters"] == cached_adhoc_filters + assert generated_form_data["column_config"] == { + "Sales": {"d3NumberFormat": "$,.2f"} + } assert result["success"] is True assert result["error"] is None assert result["warnings"] == [] From 2c31ba146de8a9818ca350d1a85dc811e9f20a54 Mon Sep 17 00:00:00 2001 From: Amin Ghadersohi Date: Tue, 4 Aug 2026 19:45:38 +0000 Subject: [PATCH 2/2] fix(mcp): preserve table column settings --- superset/mcp_service/chart/chart_utils.py | 34 ++++++++ superset/mcp_service/chart/schemas.py | 6 +- .../mcp_service/chart/tool/update_chart.py | 34 ++++---- .../chart/tool/update_chart_preview.py | 7 +- .../mcp_service/chart/test_chart_utils.py | 38 +++++++++ .../chart/tool/test_get_chart_type_schema.py | 2 + .../chart/tool/test_update_chart.py | 83 +++++++++++++++++++ .../chart/tool/test_update_chart_preview.py | 9 +- 8 files changed, 190 insertions(+), 23 deletions(-) diff --git a/superset/mcp_service/chart/chart_utils.py b/superset/mcp_service/chart/chart_utils.py index c5eb72b121e4..cba958119980 100644 --- a/superset/mcp_service/chart/chart_utils.py +++ b/superset/mcp_service/chart/chart_utils.py @@ -24,6 +24,7 @@ import hashlib import logging +from collections.abc import Mapping from dataclasses import dataclass from typing import Any, Dict, TYPE_CHECKING @@ -576,6 +577,39 @@ def map_table_config(config: TableChartConfig) -> Dict[str, Any]: return form_data +def merge_table_column_config( + existing_form_data: Mapping[str, Any], new_form_data: Dict[str, Any] +) -> None: + """Merge MCP table formatting without discarding UI-only settings. + + An omitted ``column_config`` preserves the saved value, an explicit empty + mapping clears it, and a non-empty mapping updates only the supplied labels + and properties. The nested merge is important because the Superset UI stores + additional column settings that the MCP schema does not expose. + """ + if "column_config" not in new_form_data: + if "column_config" in existing_form_data: + new_form_data["column_config"] = existing_form_data["column_config"] + return + + new_column_config = new_form_data["column_config"] + if not isinstance(new_column_config, dict) or not new_column_config: + return + + existing_column_config = existing_form_data.get("column_config") + if not isinstance(existing_column_config, dict): + return + + merged_column_config = dict(existing_column_config) + for label, settings in new_column_config.items(): + existing_settings = existing_column_config.get(label) + if isinstance(existing_settings, dict) and isinstance(settings, dict): + merged_column_config[label] = {**existing_settings, **settings} + else: + merged_column_config[label] = settings + new_form_data["column_config"] = merged_column_config + + def create_metric_object(col: ColumnRef) -> Dict[str, Any] | str: """Create a metric object for a column with enhanced validation. diff --git a/superset/mcp_service/chart/schemas.py b/superset/mcp_service/chart/schemas.py index c1fd8bc24ec0..27ebdaaed70c 100644 --- a/superset/mcp_service/chart/schemas.py +++ b/superset/mcp_service/chart/schemas.py @@ -1543,7 +1543,11 @@ def validate_metric_aggregate(self) -> Self: class TableColumnConfig(UnknownFieldCheckMixin): """Display formatting supported by the MCP table-chart schema.""" - model_config = ConfigDict(extra="ignore", populate_by_name=True) + model_config = ConfigDict( + extra="ignore", + populate_by_name=True, + json_schema_extra={"additionalProperties": False}, + ) column_width: int | None = Field( None, diff --git a/superset/mcp_service/chart/tool/update_chart.py b/superset/mcp_service/chart/tool/update_chart.py index 121293d3cd30..c26e90d91347 100644 --- a/superset/mcp_service/chart/tool/update_chart.py +++ b/superset/mcp_service/chart/tool/update_chart.py @@ -39,6 +39,7 @@ analyze_chart_semantics, generate_chart_name, map_config_to_form_data, + merge_table_column_config, ) from superset.mcp_service.chart.compile import validate_and_compile from superset.mcp_service.chart.schemas import ( @@ -59,6 +60,20 @@ logger = logging.getLogger(__name__) +def _get_existing_form_data(chart: Any) -> dict[str, Any]: + """Return a chart's saved form data, treating malformed params as empty.""" + if not getattr(chart, "params", None): + return {} + try: + parsed = json.loads(chart.params) + except (ValueError, TypeError): + parsed = None + if not isinstance(parsed, dict): + logger.warning("Failed to parse existing chart.params for chart %s", chart.id) + return {} + return parsed + + def _validation_error_response(message: str, details: str) -> GenerateChartResponse: return GenerateChartResponse.model_validate( { @@ -117,13 +132,7 @@ def _build_update_payload( parsed_config, dataset_id=effective_dataset_id ) new_form_data.pop("_mcp_warnings", None) - if getattr(parsed_config, "column_config", None) is None: - try: - existing_form_data = json.loads(chart.params) if chart.params else {} - except (ValueError, TypeError): - existing_form_data = {} - if existing_column_config := existing_form_data.get("column_config"): - new_form_data["column_config"] = existing_column_config + merge_table_column_config(_get_existing_form_data(chart), new_form_data) chart_name = ( request.chart_name @@ -171,15 +180,7 @@ def _build_preview_form_data( GenerateChartResponse error when neither config nor chart_name is given. ``parsed_config`` is the pre-parsed chart config from the caller. """ - existing_form_data: dict[str, Any] = {} - if getattr(chart, "params", None): - try: - existing_form_data = json.loads(chart.params) or {} - except (ValueError, TypeError): - logger.warning( - "Failed to parse existing chart.params for chart %s", chart.id - ) - existing_form_data = {} + existing_form_data = _get_existing_form_data(chart) effective_dataset_id = ( request.dataset_id @@ -192,6 +193,7 @@ def _build_preview_form_data( parsed_config, dataset_id=effective_dataset_id ) new_form_data.pop("_mcp_warnings", None) + merge_table_column_config(existing_form_data, new_form_data) merged = {**existing_form_data, **new_form_data} else: if not request.chart_name and request.dataset_id is None: diff --git a/superset/mcp_service/chart/tool/update_chart_preview.py b/superset/mcp_service/chart/tool/update_chart_preview.py index d394164bd735..08a6b6f03ffe 100644 --- a/superset/mcp_service/chart/tool/update_chart_preview.py +++ b/superset/mcp_service/chart/tool/update_chart_preview.py @@ -38,6 +38,7 @@ generate_chart_name, generate_explore_link, map_config_to_form_data, + merge_table_column_config, ) from superset.mcp_service.chart.compile import validate_and_compile from superset.mcp_service.chart.preview_utils import ( @@ -186,10 +187,8 @@ def update_chart_preview( # noqa: C901 old_adhoc_filters = previous_form_data.get("adhoc_filters") if old_adhoc_filters: new_form_data["adhoc_filters"] = old_adhoc_filters - if getattr(config, "column_config", None) is None and previous_form_data: - old_column_config = previous_form_data.get("column_config") - if old_column_config: - new_form_data["column_config"] = old_column_config + if previous_form_data: + merge_table_column_config(previous_form_data, new_form_data) # Tier-1 schema validation against the dataset (no DB roundtrip). # Runs AFTER the filter merge so filter columns are also validated. diff --git a/tests/unit_tests/mcp_service/chart/test_chart_utils.py b/tests/unit_tests/mcp_service/chart/test_chart_utils.py index 256ee7032c43..314d9575f4a0 100644 --- a/tests/unit_tests/mcp_service/chart/test_chart_utils.py +++ b/tests/unit_tests/mcp_service/chart/test_chart_utils.py @@ -38,6 +38,7 @@ map_filter_operator, map_table_config, map_xy_config, + merge_table_column_config, validate_chart_dataset, ) from superset.mcp_service.chart.schemas import ( @@ -144,6 +145,43 @@ def test_map_filter_operator_unknown(self) -> None: assert map_filter_operator("UNKNOWN") == "UNKNOWN" +class TestMergeTableColumnConfig: + def test_partial_update_preserves_other_labels_and_settings(self) -> None: + existing = { + "column_config": { + "Revenue": {"columnWidth": 80, "visible": False}, + "Region": {"customColumnName": "Sales region"}, + } + } + updated = { + "column_config": { + "Revenue": {"columnWidth": 120, "d3NumberFormat": "$,.2f"} + } + } + + merge_table_column_config(existing, updated) + + assert updated["column_config"] == { + "Revenue": { + "columnWidth": 120, + "d3NumberFormat": "$,.2f", + "visible": False, + }, + "Region": {"customColumnName": "Sales region"}, + } + + def test_omitted_preserves_and_explicit_empty_clears(self) -> None: + existing = {"column_config": {"Revenue": {"columnWidth": 80}}} + omitted: dict[str, Any] = {} + explicit_empty: dict[str, Any] = {"column_config": {}} + + merge_table_column_config(existing, omitted) + merge_table_column_config(existing, explicit_empty) + + assert omitted["column_config"] == existing["column_config"] + assert explicit_empty["column_config"] == {} + + class TestMapTableConfig: """Test map_table_config function""" diff --git a/tests/unit_tests/mcp_service/chart/tool/test_get_chart_type_schema.py b/tests/unit_tests/mcp_service/chart/tool/test_get_chart_type_schema.py index 1778f8c8ad9f..e1fbb27d0ae6 100644 --- a/tests/unit_tests/mcp_service/chart/tool/test_get_chart_type_schema.py +++ b/tests/unit_tests/mcp_service/chart/tool/test_get_chart_type_schema.py @@ -53,6 +53,8 @@ def test_table_schema_has_columns_and_column_config(self) -> None: assert "columnWidth" in description assert "d3NumberFormat" in description assert "d3TimeFormat" in description + column_config_schema = result["schema"]["$defs"]["TableColumnConfig"] + assert column_config_schema["additionalProperties"] is False def test_pie_schema_has_dimension_metric(self) -> None: result = _call_schema("pie") diff --git a/tests/unit_tests/mcp_service/chart/tool/test_update_chart.py b/tests/unit_tests/mcp_service/chart/tool/test_update_chart.py index 807fe2a7ad02..723ce42c4941 100644 --- a/tests/unit_tests/mcp_service/chart/tool/test_update_chart.py +++ b/tests/unit_tests/mcp_service/chart/tool/test_update_chart.py @@ -43,6 +43,7 @@ _build_preview_form_data, _build_update_payload, ) +from superset.utils import json # The __init__.py re-exports the update_chart *function*, so a plain # `from ... import update_chart` gives the function, not the module. @@ -134,6 +135,51 @@ def test_unrelated_config_update_preserves_existing_column_config(self) -> None: assert isinstance(payload, dict) assert '"column_config": {"Revenue": {"columnWidth": 160}}' in payload["params"] + @pytest.mark.parametrize("params", ["null", "[]", '"text"', "1", "true"]) + def test_config_update_treats_non_object_params_as_empty(self, params: str) -> None: + chart = Mock(datasource_id=7, params=params, slice_name="Revenue table", id=123) + request = UpdateChartRequest( + identifier=123, + config=TableChartConfig( + chart_type="table", columns=[ColumnRef(name="revenue")] + ), + ) + + payload = _build_update_payload(request, chart, request.config) + + assert isinstance(payload, dict) + assert "column_config" not in json.loads(payload["params"]) + + def test_explicit_column_update_merges_saved_ui_settings(self) -> None: + chart = Mock( + datasource_id=7, + params=json.dumps( + { + "column_config": { + "Revenue": {"columnWidth": 80, "visible": False}, + "Region": {"customColumnName": "Sales region"}, + } + } + ), + slice_name="Revenue table", + ) + config = TableChartConfig.model_validate( + { + "chart_type": "table", + "columns": [{"name": "revenue", "aggregate": "SUM"}], + "column_config": {"Revenue": {"columnWidth": 120}}, + } + ) + request = UpdateChartRequest(identifier=123, config=config) + + payload = _build_update_payload(request, chart, config) + + assert isinstance(payload, dict) + assert json.loads(payload["params"])["column_config"] == { + "Revenue": {"columnWidth": 120, "visible": False}, + "Region": {"customColumnName": "Sales region"}, + } + @pytest.mark.asyncio async def test_update_chart_preview_formats(self): """Test preview_formats options in update request.""" @@ -905,6 +951,43 @@ def test_merges_existing_params_with_new_config(self): assert result["datasource"] == "7__table" assert result["slice_name"] == "Existing" + def test_partial_column_config_merges_saved_ui_settings(self) -> None: + config = TableChartConfig.model_validate( + { + "chart_type": "table", + "columns": [{"name": "revenue", "aggregate": "SUM"}], + "column_config": { + "Revenue": {"columnWidth": 120, "d3NumberFormat": "$,.2f"} + }, + } + ) + request = UpdateChartRequest(identifier=1, config=config) + chart = Mock( + id=42, + datasource_id=7, + slice_name="Existing", + params=json.dumps( + { + "column_config": { + "Revenue": {"columnWidth": 80, "visible": False}, + "Region": {"customColumnName": "Sales region"}, + } + } + ), + ) + + result = _build_preview_form_data(request, chart, parsed_config=config) + + assert isinstance(result, dict) + assert result["column_config"] == { + "Revenue": { + "columnWidth": 120, + "d3NumberFormat": "$,.2f", + "visible": False, + }, + "Region": {"customColumnName": "Sales region"}, + } + def test_name_only_preview_keeps_existing_form_data(self): """Name-only preview preserves existing form_data and renames.""" request = UpdateChartRequest(identifier=1, chart_name="Brand New Name") diff --git a/tests/unit_tests/mcp_service/chart/tool/test_update_chart_preview.py b/tests/unit_tests/mcp_service/chart/tool/test_update_chart_preview.py index 6e126dfe13e8..b33100a07447 100644 --- a/tests/unit_tests/mcp_service/chart/tool/test_update_chart_preview.py +++ b/tests/unit_tests/mcp_service/chart/tool/test_update_chart_preview.py @@ -690,7 +690,7 @@ async def test_preserves_previous_adhoc_filters_without_warning( ] mock_get_previous_form_data.return_value = { "adhoc_filters": cached_adhoc_filters, - "column_config": {"Sales": {"d3NumberFormat": "$,.2f"}}, + "column_config": {"Sales": {"d3NumberFormat": "$,.2f", "visible": False}}, } mock_generate_explore_link.return_value = ( "http://localhost:8088/explore/?form_data_key=new_preview_key" @@ -708,6 +708,7 @@ async def test_preserves_previous_adhoc_filters_without_warning( ColumnRef(name="sales", label="Sales", aggregate="SUM"), ], sort_by=["sales"], + column_config={"Sales": {"columnWidth": 120}}, ), generate_preview=True, preview_formats=["table"], @@ -720,7 +721,11 @@ async def test_preserves_previous_adhoc_filters_without_warning( generated_form_data = mock_generate_explore_link.call_args.args[1] assert generated_form_data["adhoc_filters"] == cached_adhoc_filters assert generated_form_data["column_config"] == { - "Sales": {"d3NumberFormat": "$,.2f"} + "Sales": { + "columnWidth": 120, + "d3NumberFormat": "$,.2f", + "visible": False, + } } assert result["success"] is True assert result["error"] is None