From 5c6f3d45617d7326bfa2ffc97028892ee152c508 Mon Sep 17 00:00:00 2001 From: janmatzek Date: Tue, 12 May 2026 15:26:33 +0200 Subject: [PATCH] fix: migrate insight and dashboard tags --- README.md | 2 +- .../dashboards/cloud_dashboard.py | 7 ++++++- src/gooddata_legacy2cloud/helpers.py | 11 +++++++++++ .../insights/cloud_insight.py | 3 ++- src/gooddata_legacy2cloud/metrics/cloud_metric.py | 14 ++------------ .../cloud_objects/dashboard_objects.py | 5 ++++- src/gooddata_legacy2cloud/pp_dashboards/utils.py | 1 + .../reports/transformation.py | 14 ++++++++++---- .../test_cases/basic_dashboard_cloud.json | 1 + ...ribute_filter_with_multiple_labels_1_cloud.json | 1 + .../dashboard_with_dependent_filters_cloud.json | 1 + .../test_cases/dashboard_with_drills_cloud.json | 1 + .../dashboard_with_kpis_and_filters_cloud.json | 1 + ...ashboard_with_missing_element_lookup_cloud.json | 1 + .../test_cases/headlines_only_cloud.json | 1 + .../dashboards/test_cases/self_drill_cloud.json | 1 + .../insights/test_cases/basic_insight_cloud.json | 1 + .../insights/test_cases/basic_insight_legacy.json | 2 +- ...s_all_additional_date_labels_gd_date_cloud.json | 1 + .../test_cases/headline_as_a_kpi_cloud.json | 1 + .../test_cases/using_deprecated_metric_cloud.json | 1 + .../test_cases/with_metric_value_filter_cloud.json | 1 + ...th_missing_value_in_color_definition_cloud.json | 1 + .../insights/test_cases/with_top_filter_cloud.json | 1 + .../legacy_objects/pp_dashboard_simple.json | 1 + .../reports/test_cases/basic_reports_cloud.json | 1 + .../test_cases/date_null_filters_cloud.json | 2 ++ .../measure_filter_granularity_subset_cloud.json | 1 + tests/test_pp_dashboards_migration.py | 3 +++ 29 files changed, 61 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index c656ac1..0afb057 100644 --- a/README.md +++ b/README.md @@ -360,7 +360,7 @@ Notes: **Metrics-specific options:** -**--keep-original-ids** - Keep original metric identifiers from Legacy. Otherwise the Cloud ID is derived from metric title and Legacy identfier. +**--keep-original-ids** - Keep original metric identifiers from Legacy. Otherwise the Cloud ID is derived from metric title and Legacy identifier. **--ignore-folders** - Legacy folders of Metrics are not migrated to Cloud tags. Use if you used only tags for organizing the catalog in Legacy. diff --git a/src/gooddata_legacy2cloud/dashboards/cloud_dashboard.py b/src/gooddata_legacy2cloud/dashboards/cloud_dashboard.py index 9f299da..12e5170 100644 --- a/src/gooddata_legacy2cloud/dashboards/cloud_dashboard.py +++ b/src/gooddata_legacy2cloud/dashboards/cloud_dashboard.py @@ -14,7 +14,11 @@ DrillConverter, ) from gooddata_legacy2cloud.dashboards.filter_context import FilterContext -from gooddata_legacy2cloud.helpers import dashboard_specific_insight_id, get_cloud_id +from gooddata_legacy2cloud.helpers import ( + dashboard_specific_insight_id, + get_cloud_id, + parse_legacy_tags, +) from gooddata_legacy2cloud.insights.period_comparison_insight import ( PeriodComparisonInsight, ) @@ -491,6 +495,7 @@ def get(self): "attributes": { "title": self.title, "description": self.meta["summary"], + "tags": parse_legacy_tags(self.meta), "content": { "attributeFilterConfigs": self.attribute_filter_configs, "filterContextRef": self.filter_context_id, diff --git a/src/gooddata_legacy2cloud/helpers.py b/src/gooddata_legacy2cloud/helpers.py index 0d82ade..b2735e0 100644 --- a/src/gooddata_legacy2cloud/helpers.py +++ b/src/gooddata_legacy2cloud/helpers.py @@ -139,6 +139,17 @@ def get_object_list(input): return list(set(matches)) # get rid of duplicities +def parse_legacy_tags(meta: dict) -> list[str]: + """Parses the space/comma-separated tags string from Legacy metadata into a list.""" + tags_str = meta.get("tags", "") + return [ + tag.strip() + for part in tags_str.split(",") + for tag in part.split() + if tag.strip() + ] + + def get_cloud_id(title: str, legacy_identifier: str) -> str: """ Returns the Cloud metric identifier. diff --git a/src/gooddata_legacy2cloud/insights/cloud_insight.py b/src/gooddata_legacy2cloud/insights/cloud_insight.py index 814f68c..c681173 100644 --- a/src/gooddata_legacy2cloud/insights/cloud_insight.py +++ b/src/gooddata_legacy2cloud/insights/cloud_insight.py @@ -8,7 +8,7 @@ import logging import uuid -from gooddata_legacy2cloud.helpers import get_cloud_id +from gooddata_legacy2cloud.helpers import get_cloud_id, parse_legacy_tags from gooddata_legacy2cloud.id_mappings import IdMappings from gooddata_legacy2cloud.insights.data_classes import InsightContext from gooddata_legacy2cloud.insights.period_comparison_insight import ( @@ -518,6 +518,7 @@ def get(self): "attributes": { "title": self.title, "description": self.description, + "tags": parse_legacy_tags(self.meta), "createdAt": "", "content": { "filters": self.filters, diff --git a/src/gooddata_legacy2cloud/metrics/cloud_metric.py b/src/gooddata_legacy2cloud/metrics/cloud_metric.py index f47263c..839c801 100644 --- a/src/gooddata_legacy2cloud/metrics/cloud_metric.py +++ b/src/gooddata_legacy2cloud/metrics/cloud_metric.py @@ -7,7 +7,7 @@ import logging from typing import Any -from gooddata_legacy2cloud.helpers import get_cloud_id +from gooddata_legacy2cloud.helpers import get_cloud_id, parse_legacy_tags from gooddata_legacy2cloud.metrics.data_classes import MetricContext from gooddata_legacy2cloud.metrics.cloud_maql import CloudMaql from gooddata_legacy2cloud.metrics.utils import get_folders_names @@ -47,17 +47,7 @@ def __init__( ) def _get_tags(self): - """ - Prepares the tags for the metric. - @param tags_str: The metadata tags string. - """ - tags_str = self.meta.get("tags", "") - tags = [ - tag.strip() - for part in tags_str.split(",") - for tag in part.split() - if tag.strip() - ] + tags = parse_legacy_tags(self.meta) # add Legacy folders to Cloud tags if "folders" in self.metric_content and not self.ctx.ignore_folders: diff --git a/src/gooddata_legacy2cloud/pp_dashboards/cloud_objects/dashboard_objects.py b/src/gooddata_legacy2cloud/pp_dashboards/cloud_objects/dashboard_objects.py index 769551f..5f23031 100644 --- a/src/gooddata_legacy2cloud/pp_dashboards/cloud_objects/dashboard_objects.py +++ b/src/gooddata_legacy2cloud/pp_dashboards/cloud_objects/dashboard_objects.py @@ -4,7 +4,7 @@ from pydantic import BaseModel, Field -from gooddata_legacy2cloud.helpers import PP_DASHBOARD_PREFIX +from gooddata_legacy2cloud.helpers import PP_DASHBOARD_PREFIX, parse_legacy_tags from gooddata_legacy2cloud.pp_dashboards.legacy_objects.pixel_perfect_dashboard import ( PixelPerfectDashboard, Tab, @@ -121,6 +121,7 @@ class Attributes(BaseModel): title: str content: Content description: str | None = "" + tags: list[str] = Field(default_factory=list) class CloudDashboard(BaseModel): @@ -162,6 +163,7 @@ def create_from_legacy_definition( attributes=Attributes( title=f"[PP] {pixel_perfect_dashboard.meta.title} - {tab_idx:02} - {tab.title}", content=Content(layout=Layout(sections=[])), + tags=parse_legacy_tags(pixel_perfect_dashboard.meta.model_dump()), ), ) @@ -179,5 +181,6 @@ def create_tabbed_from_legacy_definition( attributes=Attributes( title=f"[PP] {pixel_perfect_dashboard.meta.title}", content=Content(layout=Layout(sections=[])), + tags=parse_legacy_tags(pixel_perfect_dashboard.meta.model_dump()), ), ) diff --git a/src/gooddata_legacy2cloud/pp_dashboards/utils.py b/src/gooddata_legacy2cloud/pp_dashboards/utils.py index b5445ff..378177f 100644 --- a/src/gooddata_legacy2cloud/pp_dashboards/utils.py +++ b/src/gooddata_legacy2cloud/pp_dashboards/utils.py @@ -12,6 +12,7 @@ class Meta(BaseModel): identifier: str title: str uri: str + tags: str = "" unlisted: Optional[int] = 0 diff --git a/src/gooddata_legacy2cloud/reports/transformation.py b/src/gooddata_legacy2cloud/reports/transformation.py index 50717d4..5565f59 100644 --- a/src/gooddata_legacy2cloud/reports/transformation.py +++ b/src/gooddata_legacy2cloud/reports/transformation.py @@ -9,8 +9,13 @@ """ import logging +from typing import Any -from gooddata_legacy2cloud.helpers import REPORT_INSIGHT_PREFIX, get_cloud_id +from gooddata_legacy2cloud.helpers import ( + REPORT_INSIGHT_PREFIX, + get_cloud_id, + parse_legacy_tags, +) from gooddata_legacy2cloud.reports.charts import process_chart_report from gooddata_legacy2cloud.reports.data_classes import ( ContextWithWarnings, @@ -87,13 +92,14 @@ def transform_legacy_report( meta.get("identifier", "unknown"), top_level_id ) - cloud_json = { + cloud_json: dict[str, Any] = { "data": { "id": top_level_id, "type": "visualizationObject", "attributes": { "title": legacy_title, "description": legacy_summary, + "tags": parse_legacy_tags(meta), "content": cloud_content, }, } @@ -103,7 +109,7 @@ def transform_legacy_report( warnings_list = warning_collector.get_warnings() errors_list = warning_collector.get_errors() if errors_list or warnings_list: - old_title = cloud_json["data"]["attributes"].get("title", "") + old_title = legacy_title if errors_list: new_prefix = "[ERROR] " elif ( @@ -116,7 +122,7 @@ def transform_legacy_report( if new_prefix and not old_title.startswith(new_prefix): cloud_json["data"]["attributes"]["title"] = new_prefix + old_title - old_description = cloud_json["data"]["attributes"].get("description", "") + old_description = legacy_summary messages_str = "" if errors_list: diff --git a/tests/data/dashboards/test_cases/basic_dashboard_cloud.json b/tests/data/dashboards/test_cases/basic_dashboard_cloud.json index 532a0f8..ec96af2 100644 --- a/tests/data/dashboards/test_cases/basic_dashboard_cloud.json +++ b/tests/data/dashboards/test_cases/basic_dashboard_cloud.json @@ -6,6 +6,7 @@ "attributes": { "title": "Migration sample", "description": "", + "tags": [], "content": { "attributeFilterConfigs": [], "filterContextRef": { diff --git a/tests/data/dashboards/test_cases/dashboard_with_attribute_filter_with_multiple_labels_1_cloud.json b/tests/data/dashboards/test_cases/dashboard_with_attribute_filter_with_multiple_labels_1_cloud.json index 45a3044..a735b7f 100644 --- a/tests/data/dashboards/test_cases/dashboard_with_attribute_filter_with_multiple_labels_1_cloud.json +++ b/tests/data/dashboards/test_cases/dashboard_with_attribute_filter_with_multiple_labels_1_cloud.json @@ -6,6 +6,7 @@ "attributes": { "title": "dashboard with attribute filter with multiple labels 1", "description": "", + "tags": [], "content": { "attributeFilterConfigs": [ { diff --git a/tests/data/dashboards/test_cases/dashboard_with_dependent_filters_cloud.json b/tests/data/dashboards/test_cases/dashboard_with_dependent_filters_cloud.json index 0062a0c..90852c3 100644 --- a/tests/data/dashboards/test_cases/dashboard_with_dependent_filters_cloud.json +++ b/tests/data/dashboards/test_cases/dashboard_with_dependent_filters_cloud.json @@ -6,6 +6,7 @@ "attributes": { "title": "Dashboard with dependent filters", "description": "", + "tags": [], "content": { "attributeFilterConfigs": [], "filterContextRef": { diff --git a/tests/data/dashboards/test_cases/dashboard_with_drills_cloud.json b/tests/data/dashboards/test_cases/dashboard_with_drills_cloud.json index 4e50371..18797e8 100644 --- a/tests/data/dashboards/test_cases/dashboard_with_drills_cloud.json +++ b/tests/data/dashboards/test_cases/dashboard_with_drills_cloud.json @@ -6,6 +6,7 @@ "attributes": { "title": "Dashboard with Drills", "description": "", + "tags": ["important", "is", "this", "very"], "content": { "attributeFilterConfigs": [], "filterContextRef": { diff --git a/tests/data/dashboards/test_cases/dashboard_with_kpis_and_filters_cloud.json b/tests/data/dashboards/test_cases/dashboard_with_kpis_and_filters_cloud.json index a9ef784..3848b54 100644 --- a/tests/data/dashboards/test_cases/dashboard_with_kpis_and_filters_cloud.json +++ b/tests/data/dashboards/test_cases/dashboard_with_kpis_and_filters_cloud.json @@ -6,6 +6,7 @@ "attributes": { "title": "Dashboard WITH KPIs and filters", "description": "", + "tags": [], "content": { "attributeFilterConfigs": [], "filterContextRef": { diff --git a/tests/data/dashboards/test_cases/dashboard_with_missing_element_lookup_cloud.json b/tests/data/dashboards/test_cases/dashboard_with_missing_element_lookup_cloud.json index 09a77e8..c5e8565 100644 --- a/tests/data/dashboards/test_cases/dashboard_with_missing_element_lookup_cloud.json +++ b/tests/data/dashboards/test_cases/dashboard_with_missing_element_lookup_cloud.json @@ -6,6 +6,7 @@ "attributes": { "title": "[WARN] Dashboard with missing element lookup", "description": "", + "tags": [], "content": { "attributeFilterConfigs": [], "filterContextRef": { diff --git a/tests/data/dashboards/test_cases/headlines_only_cloud.json b/tests/data/dashboards/test_cases/headlines_only_cloud.json index 18de474..ac69e9a 100644 --- a/tests/data/dashboards/test_cases/headlines_only_cloud.json +++ b/tests/data/dashboards/test_cases/headlines_only_cloud.json @@ -6,6 +6,7 @@ "attributes": { "title": "headlines only", "description": "", + "tags": [], "content": { "attributeFilterConfigs": [], "filterContextRef": { diff --git a/tests/data/dashboards/test_cases/self_drill_cloud.json b/tests/data/dashboards/test_cases/self_drill_cloud.json index 7e0ea3d..d2cd1e8 100644 --- a/tests/data/dashboards/test_cases/self_drill_cloud.json +++ b/tests/data/dashboards/test_cases/self_drill_cloud.json @@ -6,6 +6,7 @@ "attributes": { "title": "self drill", "description": "", + "tags": [], "content": { "attributeFilterConfigs": [], "filterContextRef": { diff --git a/tests/data/insights/test_cases/basic_insight_cloud.json b/tests/data/insights/test_cases/basic_insight_cloud.json index cb7aa8c..26a4734 100644 --- a/tests/data/insights/test_cases/basic_insight_cloud.json +++ b/tests/data/insights/test_cases/basic_insight_cloud.json @@ -6,6 +6,7 @@ "attributes": { "title": "Avg item price per category", "description": "", + "tags": ["finance", "revenue"], "createdAt": "", "content": { "filters": [], diff --git a/tests/data/insights/test_cases/basic_insight_legacy.json b/tests/data/insights/test_cases/basic_insight_legacy.json index d7b5e5c..bcdcbcf 100644 --- a/tests/data/insights/test_cases/basic_insight_legacy.json +++ b/tests/data/insights/test_cases/basic_insight_legacy.json @@ -13,7 +13,7 @@ "summary": "", "author": "/gdc/account/profile/5cc081c981561ae1d3f81481b50f002c", "identifier": "abb6SWS81yFv", - "tags": "" + "tags": "finance revenue" }, "content": { "buckets": [ diff --git a/tests/data/insights/test_cases/has_all_additional_date_labels_gd_date_cloud.json b/tests/data/insights/test_cases/has_all_additional_date_labels_gd_date_cloud.json index c677708..631e617 100644 --- a/tests/data/insights/test_cases/has_all_additional_date_labels_gd_date_cloud.json +++ b/tests/data/insights/test_cases/has_all_additional_date_labels_gd_date_cloud.json @@ -6,6 +6,7 @@ "attributes": { "title": "has all additional date labels (GD date)", "description": "", + "tags": [], "createdAt": "", "content": { "filters": [], diff --git a/tests/data/insights/test_cases/headline_as_a_kpi_cloud.json b/tests/data/insights/test_cases/headline_as_a_kpi_cloud.json index 4815ef4..3e6e9a8 100644 --- a/tests/data/insights/test_cases/headline_as_a_kpi_cloud.json +++ b/tests/data/insights/test_cases/headline_as_a_kpi_cloud.json @@ -6,6 +6,7 @@ "attributes": { "title": "Headline as a KPI", "description": "", + "tags": [], "createdAt": "", "content": { "filters": [ diff --git a/tests/data/insights/test_cases/using_deprecated_metric_cloud.json b/tests/data/insights/test_cases/using_deprecated_metric_cloud.json index 1a8b1d1..e893dbe 100644 --- a/tests/data/insights/test_cases/using_deprecated_metric_cloud.json +++ b/tests/data/insights/test_cases/using_deprecated_metric_cloud.json @@ -6,6 +6,7 @@ "attributes": { "title": "using deprecated metric", "description": "", + "tags": [], "createdAt": "", "content": { "filters": [], diff --git a/tests/data/insights/test_cases/with_metric_value_filter_cloud.json b/tests/data/insights/test_cases/with_metric_value_filter_cloud.json index 231f507..5291205 100644 --- a/tests/data/insights/test_cases/with_metric_value_filter_cloud.json +++ b/tests/data/insights/test_cases/with_metric_value_filter_cloud.json @@ -6,6 +6,7 @@ "attributes": { "title": "with metric value filter", "description": "", + "tags": [], "createdAt": "", "content": { "filters": [ diff --git a/tests/data/insights/test_cases/with_missing_value_in_color_definition_cloud.json b/tests/data/insights/test_cases/with_missing_value_in_color_definition_cloud.json index 85baaa2..182eff8 100644 --- a/tests/data/insights/test_cases/with_missing_value_in_color_definition_cloud.json +++ b/tests/data/insights/test_cases/with_missing_value_in_color_definition_cloud.json @@ -6,6 +6,7 @@ "attributes": { "title": "[WARN] with missing value in color definition", "description": "\nMigration errors - missing values in filters: {'generic filters - positiveAttributeFilter: customer.country': ['/gdc/md/fkxyvp08rrrkfqss1ai656hvs0m77vl0/obj/633/elements?id=2527', '/gdc/md/fkxyvp08rrrkfqss1ai656hvs0m77vl0/obj/633/elements?id=2525']}\nMissing values in color mapping: {'colorMapping - 93e3f96a07ae4856b05b0294e743efde': '/gdc/md/fkxyvp08rrrkfqss1ai656hvs0m77vl0/obj/631/elements?id=2526', 'colorMapping - 5bd71afd702641e1bc2d795fcecae37d': '/gdc/md/fkxyvp08rrrkfqss1ai656hvs0m77vl0/obj/631/elements?id=2524'}", + "tags": [], "createdAt": "", "content": { "filters": [ diff --git a/tests/data/insights/test_cases/with_top_filter_cloud.json b/tests/data/insights/test_cases/with_top_filter_cloud.json index 5b23fa2..bc23840 100644 --- a/tests/data/insights/test_cases/with_top_filter_cloud.json +++ b/tests/data/insights/test_cases/with_top_filter_cloud.json @@ -6,6 +6,7 @@ "attributes": { "title": "with TOP filter", "description": "", + "tags": [], "createdAt": "", "content": { "filters": [ diff --git a/tests/data/pixel_perfect_dashboards/legacy_objects/pp_dashboard_simple.json b/tests/data/pixel_perfect_dashboards/legacy_objects/pp_dashboard_simple.json index c9cf750..eab06d2 100644 --- a/tests/data/pixel_perfect_dashboards/legacy_objects/pp_dashboard_simple.json +++ b/tests/data/pixel_perfect_dashboards/legacy_objects/pp_dashboard_simple.json @@ -7,6 +7,7 @@ "title": "Test Pixel Perfect Dashboard", "uri": "/gdc/md/test_workspace/obj/9001", "summary": "Test dashboard for migration", + "tags": "finance revenue", "unlisted": 0 }, "content": { diff --git a/tests/data/reports/test_cases/basic_reports_cloud.json b/tests/data/reports/test_cases/basic_reports_cloud.json index a4aa26f..bd115cc 100644 --- a/tests/data/reports/test_cases/basic_reports_cloud.json +++ b/tests/data/reports/test_cases/basic_reports_cloud.json @@ -6,6 +6,7 @@ "attributes": { "title": "[PP] sample report", "description": "", + "tags": [], "content": { "buckets": [ { diff --git a/tests/data/reports/test_cases/date_null_filters_cloud.json b/tests/data/reports/test_cases/date_null_filters_cloud.json index a3f1fd5..8302525 100644 --- a/tests/data/reports/test_cases/date_null_filters_cloud.json +++ b/tests/data/reports/test_cases/date_null_filters_cloud.json @@ -6,6 +6,7 @@ "attributes": { "title": "[WARN] [PP] manual date is null", "description": "**migration warnings:**\n* Failed to create date NULL filter: Search Cloud Id - Unknown Cloud identifier signupdate.year\n\n---\n", + "tags": [], "content": { "buckets": [ { @@ -63,6 +64,7 @@ "attributes": { "title": "[WARN] [PP] manual date is not null", "description": "**migration warnings:**\n* Failed to create date NULL filter: Search Cloud Id - Unknown Cloud identifier signupdate.year\n\n---\n", + "tags": [], "content": { "buckets": [ { diff --git a/tests/data/reports/test_cases/measure_filter_granularity_subset_cloud.json b/tests/data/reports/test_cases/measure_filter_granularity_subset_cloud.json index ba4b848..73c351a 100644 --- a/tests/data/reports/test_cases/measure_filter_granularity_subset_cloud.json +++ b/tests/data/reports/test_cases/measure_filter_granularity_subset_cloud.json @@ -6,6 +6,7 @@ "attributes": { "title": "[PP] measure filter granularity subset", "description": "", + "tags": [], "content": { "buckets": [ { diff --git a/tests/test_pp_dashboards_migration.py b/tests/test_pp_dashboards_migration.py index f8c6c88..f26f765 100644 --- a/tests/test_pp_dashboards_migration.py +++ b/tests/test_pp_dashboards_migration.py @@ -57,6 +57,9 @@ def test_pp_dashboard_migration( dumped = cloud_dashboards[0].model_dump(exclude_none=True) assert "filterContextRef" not in dumped["attributes"]["content"] + # Tags migrated from legacy meta + assert cloud_dashboards[0].attributes.tags == ["finance", "revenue"] + # Smoke-check IDs and titles for dashboard in cloud_dashboards: assert dashboard.id.startswith("ppdash")