From 48dc249ec1982aa56f5d6c2416e765f99aca0f4b Mon Sep 17 00:00:00 2001 From: Jan Soubusta Date: Tue, 7 Apr 2026 09:42:55 +0200 Subject: [PATCH] fix: pass entities arg to WASM visualization/dashboard converters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The WASM converter functions declarativeVisualisationToYaml and declarativeDashboardToYaml require an entities array as their first argument. The Python wrappers were only passing the declarative dict, which the WASM received as the entities param, leaving the actual object undefined — causing "cannot read property 'content' of undefined" for every visualization and dashboard. Co-authored-by: Claude Opus 4.6 (1M context) JIRA: DX-326 risk: low --- .../src/gooddata_sdk/catalog/workspace/aac.py | 30 ++++-- .../gooddata-sdk/tests/catalog/test_aac.py | 95 +++++++++++++++++++ 2 files changed, 119 insertions(+), 6 deletions(-) diff --git a/packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/aac.py b/packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/aac.py index 59eeffc64..0f59bbd06 100644 --- a/packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/aac.py +++ b/packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/aac.py @@ -188,14 +188,32 @@ def declarative_metric_to_aac(declarative: dict[str, Any]) -> dict[str, Any]: return declarative_metric_to_yaml(declarative) -def declarative_visualization_to_aac(declarative: dict[str, Any]) -> dict[str, Any]: - """Convert a declarative visualization dict to AAC format.""" - return declarative_visualisation_to_yaml(declarative) +def declarative_visualization_to_aac( + declarative: dict[str, Any], + entities: list[dict[str, Any]] | None = None, +) -> dict[str, Any]: + """Convert a declarative visualization dict to AAC format. + + Args: + declarative: The declarative visualization dict. + entities: Optional entities list for cross-reference resolution. + """ + ent = entities if entities is not None else [] + return declarative_visualisation_to_yaml(ent, declarative) -def declarative_dashboard_to_aac(declarative: dict[str, Any]) -> dict[str, Any]: - """Convert a declarative dashboard dict to AAC format.""" - return declarative_dashboard_to_yaml(declarative) +def declarative_dashboard_to_aac( + declarative: dict[str, Any], + entities: list[dict[str, Any]] | None = None, +) -> dict[str, Any]: + """Convert a declarative dashboard dict to AAC format. + + Args: + declarative: The declarative dashboard dict. + entities: Optional entities list for cross-reference resolution. + """ + ent = entities if entities is not None else [] + return declarative_dashboard_to_yaml(ent, declarative) def declarative_plugin_to_aac(declarative: dict[str, Any]) -> dict[str, Any]: diff --git a/packages/gooddata-sdk/tests/catalog/test_aac.py b/packages/gooddata-sdk/tests/catalog/test_aac.py index f4ccfe741..64347d3e4 100644 --- a/packages/gooddata-sdk/tests/catalog/test_aac.py +++ b/packages/gooddata-sdk/tests/catalog/test_aac.py @@ -11,6 +11,7 @@ aac_visualization_to_declarative, declarative_dataset_to_aac, declarative_metric_to_aac, + declarative_visualization_to_aac, detect_yaml_format, load_aac_workspace_from_disk, store_aac_workspace_to_disk, @@ -176,6 +177,38 @@ def test_dataset_declarative_to_aac(self) -> None: assert result["json"]["id"] == "orders" assert isinstance(result["content"], str) + def test_visualization_declarative_to_aac(self) -> None: + """Test declarative → AAC for visualizations (round-trip from fixture).""" + content = yaml.safe_load((_FIXTURES_DIR / "visualisations" / "ratings.yaml").read_text()) + declarative = aac_visualization_to_declarative(content) + result = declarative_visualization_to_aac(declarative) + assert result["json"]["id"] == "71bdc379-384a-4eac-9627-364ea847d977" + assert isinstance(result["content"], str) + assert "Ratings" in result["content"] + + def test_visualization_declarative_to_aac_inline(self) -> None: + """Test declarative → AAC for a visualization built from inline data.""" + aac_input = { + "type": "table", + "id": "my_table", + "title": "My Table", + "query": { + "fields": { + "m1": { + "title": "Sum of Amount", + "aggregation": "SUM", + "using": "fact/amount", + }, + }, + }, + "metrics": [{"field": "m1", "format": "#,##0"}], + } + declarative = aac_visualization_to_declarative(aac_input) + result = declarative_visualization_to_aac(declarative) + assert result["json"]["id"] == "my_table" + assert isinstance(result["content"], str) + assert "my_table" in result["content"] + # --------------------------------------------------------------------------- # Format detection tests @@ -263,6 +296,68 @@ def test_store_and_reload_metrics(self, tmp_path: Path) -> None: reloaded_dict = reloaded.to_dict(camel_case=True) assert len(reloaded_dict.get("analytics", {}).get("metrics", [])) == 2 + def test_store_and_reload_visualizations(self, tmp_path: Path) -> None: + """Store visualization AAC files via store_aac_workspace_to_disk, then reload.""" + aac_vis = { + "type": "table", + "id": "test_vis", + "title": "Test Visualization", + "query": { + "fields": { + "m1": { + "title": "Sum of Amount", + "aggregation": "SUM", + "using": "fact/amount", + }, + }, + }, + "metrics": [{"field": "m1", "format": "#,##0"}], + } + vis_declarative = [aac_visualization_to_declarative(aac_vis)] + + model_dict = {"analytics": {"visualizationObjects": vis_declarative}} + from gooddata_sdk.catalog.workspace.declarative_model.workspace.workspace import ( + CatalogDeclarativeWorkspaceModel, + ) + + model = CatalogDeclarativeWorkspaceModel.from_dict(model_dict) + store_aac_workspace_to_disk(model, tmp_path) + + # Verify files were created + vis_files = list((tmp_path / "visualisations").glob("*.yaml")) + assert len(vis_files) == 1 + + # Reload + reloaded = load_aac_workspace_from_disk(tmp_path) + reloaded_dict = reloaded.to_dict(camel_case=True) + assert len(reloaded_dict.get("analytics", {}).get("visualizationObjects", [])) == 1 + + def test_store_and_reload_from_fixtures(self, tmp_path: Path) -> None: + """Load fixtures, store to disk, reload — full round-trip.""" + import shutil + import tempfile + + with tempfile.TemporaryDirectory() as tmp: + fixture_path = Path(tmp) + for subdir in ("datasets", "metrics", "visualisations"): + src = _FIXTURES_DIR / subdir + if src.exists(): + shutil.copytree(src, fixture_path / subdir) + + model = load_aac_workspace_from_disk(fixture_path) + + store_aac_workspace_to_disk(model, tmp_path) + + # Verify visualization files exist + vis_files = list((tmp_path / "visualisations").glob("*.yaml")) + assert len(vis_files) == 2 + + # Reload and verify counts match + reloaded = load_aac_workspace_from_disk(tmp_path) + reloaded_dict = reloaded.to_dict(camel_case=True) + assert len(reloaded_dict.get("analytics", {}).get("visualizationObjects", [])) == 2 + assert len(reloaded_dict.get("analytics", {}).get("metrics", [])) == 1 + def test_load_ignores_non_workspace_dirs(self, tmp_path: Path) -> None: """Ensure load skips declarative non-workspace directories.""" # Create AAC file