From 6155990ebf2b666e3123a71814521934d7dc6463 Mon Sep 17 00:00:00 2001 From: janmatzek Date: Wed, 13 May 2026 11:48:42 +0200 Subject: [PATCH] feat: add --keep-original-ids to insights and dashboards --- README.md | 12 ++++- .../arg_parsing/arg_parser.py | 28 ++++++++---- .../config/configuration_objects.py | 23 ++++++++++ .../dashboards/cloud_dashboard.py | 25 ++++++----- .../dashboards/data_classes.py | 1 + .../insights/cloud_insight.py | 9 ++-- .../insights/data_classes.py | 1 + .../cloud_objects/dashboard_objects.py | 14 ++++-- .../cloud_pp_dashboards_builder.py | 8 +++- .../pp_dashboards/data_classes.py | 1 + .../reports/data_classes.py | 1 + .../reports/transformation.py | 15 ++++--- .../workflows/migrate_dashboards.py | 1 + .../workflows/migrate_insights.py | 1 + .../migrate_pixel_perfect_dashboards.py | 1 + .../workflows/migrate_reports.py | 1 + tests/fixtures/dashboards_fixtures.py | 45 +++++++++++++++++++ tests/fixtures/insights_fixtures.py | 31 +++++++++++++ tests/fixtures/pp_dashboards_fixtures.py | 33 ++++++++++---- tests/test_configuration_validation.py | 14 ++++++ tests/test_dashboards.py | 22 ++++++++- tests/test_insights.py | 15 ++++++- tests/test_pp_dashboards_migration.py | 29 ++++++++++++ 23 files changed, 289 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 0afb057..4b90d09 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,6 @@ The migration can be performed as a complete workspace transfer or selectively f - [Understanding Mapping Files](#understanding-mapping-files) - [Custom Client Object Migration](#custom-client-object-migration) - [Advanced Mapping File Parameters](#advanced-mapping-file-parameters) -- [Generate DDL from Cloud's LDM](#generate-ddl-from-clouds-ldm) - [Web Comparison Tool](#web-comparison-tool) - [Standalone Tools](#standalone-tools) @@ -404,6 +403,10 @@ Notes: - Objects that already exist in Cloud (based on their ID) are skipped and their IDs are recorded in the skipped objects file (unless `--overwrite-existing` is used). - For best results with element lookup, use `--validation-element-lookup-with-metrics` (see [Element Lookup Parameters](#element-lookup-parameters)) +**Insights-specific options:** + +**--keep-original-ids** - Keep original Legacy identifiers as Cloud IDs instead of generating new ones. Otherwise the Cloud ID is derived from the insight title and Legacy identifier. + ### Dashboards Migration ```bash @@ -432,6 +435,10 @@ This script only migrates the Responsive Dashboards (a.k.a. KPI Dashboards). The - Objects that already exist in Cloud (based on their ID) are skipped and their IDs are recorded in the skipped objects file (unless `--overwrite-existing` is used). - For best results with element lookup, use `--validation-element-lookup-with-metrics` (see [Element Lookup Parameters](#element-lookup-parameters)) +**Dashboards-specific options:** + +**--keep-original-ids** - Keep original Legacy identifiers as Cloud IDs instead of generating new ones. Otherwise the Cloud ID is derived from the dashboard title and Legacy identifier. + ### Pixel Perfect Dashboards Migration ```bash @@ -442,6 +449,7 @@ Notes: - By default, each Legacy Pixel Perfect dashboard is migrated **one-to-one** into a single Cloud KPI dashboard that uses **native tabs** (one Legacy tab -> one Cloud tab). - Use `--pp-legacy-split-tabs` to enable the legacy behavior where each Legacy tab is migrated as a separate Cloud dashboard (intended for transition only). +- Use `--keep-original-ids` to keep the Legacy PP dashboard identifier as the Cloud dashboard ID. Cannot be combined with `--pp-legacy-split-tabs`. Note that dashboards created with this flag will not be removed when `--cleanup-target-env` is used AND will get removed if you migrate regular dashboards with `--cleanup-target-env` after this. ### Reports Migration @@ -473,6 +481,8 @@ Notes: **--report-prefix** - Prefix added to the visualziations migrated from PixelPerfect reports to distinguish them from those migrated from Insights. Default is '[PP] '. Use empty string to disable the prefix. +**--keep-original-ids** - Keep original metric identifiers from Legacy. Otherwise, the Cloud ID is derived from metric title and Legacy identifier. Note that insights created with this flag will not be removed when `--cleanup-target-env` is used AND will get removed if you migrate regular insights with `--cleanup-target-env` after this. + ### Scheduled Exports Migration ```bash diff --git a/src/gooddata_legacy2cloud/arg_parsing/arg_parser.py b/src/gooddata_legacy2cloud/arg_parsing/arg_parser.py index 3685b75..e666946 100644 --- a/src/gooddata_legacy2cloud/arg_parsing/arg_parser.py +++ b/src/gooddata_legacy2cloud/arg_parsing/arg_parser.py @@ -420,6 +420,21 @@ def add_validation_element_lookup_with_metrics_argument( ) +def add_keep_original_ids_argument(parser: argparse.ArgumentParser) -> None: + """ + Adds keep-original-ids argument to the parser. + """ + parser.add_argument( + "--keep-original-ids", + action="store_const", + const=True, + dest="keep_original_ids", + default=False, + help="Keep the original Legacy identifiers as Cloud IDs instead of " + + "deriving new IDs from the object title and Legacy identifier.", + ) + + def add_cleanup_target_env_argument( parser: argparse.ArgumentParser, object_type: str = "objects" ) -> None: @@ -485,6 +500,7 @@ def parse_report_cli_args() -> argparse.Namespace: add_validation_element_lookup_argument(parser) add_element_values_prefetch_argument(parser) add_cleanup_target_env_argument(parser, object_type="reports") + add_keep_original_ids_argument(parser) parser.add_argument( "--report-prefix", @@ -513,6 +529,7 @@ def parse_pixel_perfect_dashboard_cli_args() -> argparse.Namespace: add_insight_mapping_arguments(parser) add_validation_element_lookup_argument(parser) add_pp_dashboard_arguments(parser) + add_keep_original_ids_argument(parser) args = parser.parse_args() return args @@ -529,14 +546,7 @@ def parse_metric_cli_args() -> argparse.Namespace: add_element_values_prefetch_argument(parser) add_cleanup_target_env_argument(parser, object_type="metrics") - parser.add_argument( - "--keep-original-ids", - action="store_const", - const=True, - dest="keep_original_ids", - default=False, - help="It will keep the original ids of the metrics.", - ) + add_keep_original_ids_argument(parser) parser.add_argument( "--ignore-folders", @@ -593,6 +603,7 @@ def parse_insight_cli_args() -> argparse.Namespace: add_validation_element_lookup_argument(parser) add_element_values_prefetch_argument(parser) add_validation_element_lookup_with_metrics_argument(parser) + add_keep_original_ids_argument(parser) add_cleanup_target_env_argument(parser, object_type="insights") args = parser.parse_args() @@ -613,6 +624,7 @@ def parse_dashboard_cli_args() -> argparse.Namespace: add_validation_element_lookup_argument(parser) add_element_values_prefetch_argument(parser) add_validation_element_lookup_with_metrics_argument(parser) + add_keep_original_ids_argument(parser) add_cleanup_target_env_argument(parser, object_type="dashboards") parser.add_argument( diff --git a/src/gooddata_legacy2cloud/config/configuration_objects.py b/src/gooddata_legacy2cloud/config/configuration_objects.py index ee4aa0f..b991385 100644 --- a/src/gooddata_legacy2cloud/config/configuration_objects.py +++ b/src/gooddata_legacy2cloud/config/configuration_objects.py @@ -222,6 +222,11 @@ class ReportConfig(BaseConfig): description="Override the default '[PP] ' prefix for migrated reports. " + "Use empty string to disable the prefix.", ) + keep_original_ids: bool = Field( + default=False, + description="Keep original Legacy identifiers as Cloud IDs instead of " + + "deriving new IDs from the object title and Legacy identifier.", + ) def model_post_init(self, __context) -> None: validate_config(self) @@ -285,9 +290,19 @@ class PixelPerfectDashboardConfig(BaseConfig): + "separate KPI dashboard. By default, PP dashboards are migrated " + "one-to-one as a single tabbed KPI dashboard.", ) + keep_original_ids: bool = Field( + default=False, + description="Keep original Legacy identifiers as Cloud IDs instead of generating new ones.", + ) def model_post_init(self, __context) -> None: validate_config(self) + if self.keep_original_ids and self.pp_legacy_split_tabs: + raise ValueError( + "`--keep-original-ids` cannot be used together with `--pp-legacy-split-tabs`. " + "Split-tabs mode creates one Cloud dashboard per Legacy tab, which would cause " + "ID collisions when keeping original IDs." + ) @classmethod def from_kwargs(cls, **kwargs: Any) -> Self: @@ -355,6 +370,10 @@ class DashboardConfig(BaseConfig): + "the cache with these elements. The temporary metrics are automatically " + "deleted afterward.", ) + keep_original_ids: bool = Field( + default=False, + description="Keep original Legacy identifiers as Cloud IDs instead of generating new ones.", + ) # Custom dashboard arguments dashboard_type: str = Field( @@ -424,6 +443,10 @@ class InsightConfig(BaseConfig): + "containing unmapped elements. Running workspace validation populates " + "the cache with these elements. The temporary metrics are automatically deleted afterward.", ) + keep_original_ids: bool = Field( + default=False, + description="Keep original Legacy identifiers as Cloud IDs instead of generating new ones.", + ) def model_post_init(self, __context) -> None: validate_config(self) diff --git a/src/gooddata_legacy2cloud/dashboards/cloud_dashboard.py b/src/gooddata_legacy2cloud/dashboards/cloud_dashboard.py index 12e5170..f7c0fc3 100644 --- a/src/gooddata_legacy2cloud/dashboards/cloud_dashboard.py +++ b/src/gooddata_legacy2cloud/dashboards/cloud_dashboard.py @@ -4,6 +4,7 @@ which is responsible for transforming the Legacy dashboard to Cloud format. """ +import json import logging from dataclasses import dataclass from typing import Any, Literal @@ -58,9 +59,12 @@ def __init__( self.overwrite_existing = overwrite_existing # Set cloud_dashboard_id early, before layout processing # because drill conversion needs it for self-referencing drills - self.cloud_dashboard_id = get_cloud_id( - self.meta["title"], self.meta["identifier"] - ) + if self.ctx.keep_original_ids: + self.cloud_dashboard_id = self.meta["identifier"] + else: + self.cloud_dashboard_id = get_cloud_id( + self.meta["title"], self.meta["identifier"] + ) self.ctx.mapping_logger.write_identifier_relation( self.meta["identifier"], self.cloud_dashboard_id ) @@ -322,11 +326,14 @@ def _get_widget(self, widget_uri: str) -> WidgetWrapper: "drills": [], } if "kpi" in obj: - # Pass the dashboard ID to make the insight ID unique - dashboard_id = self.meta.get("identifier", "") - new_insight_id = dashboard_specific_insight_id( - obj["kpi"]["meta"]["title"], dashboard_id - ) + if self.ctx.keep_original_ids: + new_insight_id = obj["kpi"]["meta"]["identifier"] + else: + # Pass the dashboard ID to make the insight ID unique + dashboard_id = self.meta.get("identifier", "") + new_insight_id = dashboard_specific_insight_id( + obj["kpi"]["meta"]["title"], dashboard_id + ) widget_object["description"] = obj["kpi"]["meta"]["summary"] widget_object["ignoreDashboardFilters"] = ( self._get_ignore_dashboard_filters(obj, "kpi") @@ -371,8 +378,6 @@ def _get_widget(self, widget_uri: str) -> WidgetWrapper: if "properties" in obj["visualizationWidget"]["content"]: # Parse properties JSON string from Legacy try: - import json - properties_str = obj["visualizationWidget"]["content"]["properties"] properties = json.loads(properties_str) widget_object["properties"] = properties diff --git a/src/gooddata_legacy2cloud/dashboards/data_classes.py b/src/gooddata_legacy2cloud/dashboards/data_classes.py index e47c69b..69ffdea 100644 --- a/src/gooddata_legacy2cloud/dashboards/data_classes.py +++ b/src/gooddata_legacy2cloud/dashboards/data_classes.py @@ -23,3 +23,4 @@ class DashboardContext: client_prefix: str | None = field(default=None) current_batch_dashboard_mappings: dict[str, str] | None = field(default=None) dashboard_type: str = field(default="analyticsDashboard") + keep_original_ids: bool = field(default=False) diff --git a/src/gooddata_legacy2cloud/insights/cloud_insight.py b/src/gooddata_legacy2cloud/insights/cloud_insight.py index c681173..654fa04 100644 --- a/src/gooddata_legacy2cloud/insights/cloud_insight.py +++ b/src/gooddata_legacy2cloud/insights/cloud_insight.py @@ -50,9 +50,12 @@ def __init__( self.visualization_url = self._get_visualization_url( self.insights_content["visualizationClass"]["uri"] ) - self.cloud_insight_id = get_cloud_id( - self.meta["title"], self.meta["identifier"] - ) + if self.ctx.keep_original_ids: + self.cloud_insight_id = self.meta["identifier"] + else: + self.cloud_insight_id = get_cloud_id( + self.meta["title"], self.meta["identifier"] + ) self.title, self.description = self._get_title_and_description() def _get_title_and_description(self): diff --git a/src/gooddata_legacy2cloud/insights/data_classes.py b/src/gooddata_legacy2cloud/insights/data_classes.py index e063f41..ab45467 100644 --- a/src/gooddata_legacy2cloud/insights/data_classes.py +++ b/src/gooddata_legacy2cloud/insights/data_classes.py @@ -18,3 +18,4 @@ class InsightContext: report_mappings: IdMappings | None = field(default=None) suppress_warnings: bool = field(default=False) client_prefix: str | None = field(default=None) + keep_original_ids: bool = field(default=False) 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 5f23031..5774545 100644 --- a/src/gooddata_legacy2cloud/pp_dashboards/cloud_objects/dashboard_objects.py +++ b/src/gooddata_legacy2cloud/pp_dashboards/cloud_objects/dashboard_objects.py @@ -169,15 +169,21 @@ def create_from_legacy_definition( @classmethod def create_tabbed_from_legacy_definition( - cls, pixel_perfect_dashboard: PixelPerfectDashboard + cls, + pixel_perfect_dashboard: PixelPerfectDashboard, + keep_original_ids: bool = False, ): """Create a single tabbed Cloud dashboard from a Legacy PP dashboard.""" - return cls( - id=get_migration_id( + if keep_original_ids: + dashboard_id = pixel_perfect_dashboard.meta.identifier + else: + dashboard_id = get_migration_id( prefix=PP_DASHBOARD_PREFIX, legacy_identifier=pixel_perfect_dashboard.meta.identifier, legacy_title=f"{pixel_perfect_dashboard.meta.title}", - ), + ) + return cls( + id=dashboard_id, attributes=Attributes( title=f"[PP] {pixel_perfect_dashboard.meta.title}", content=Content(layout=Layout(sections=[])), diff --git a/src/gooddata_legacy2cloud/pp_dashboards/cloud_pp_dashboards_builder.py b/src/gooddata_legacy2cloud/pp_dashboards/cloud_pp_dashboards_builder.py index 7f3c242..89a487e 100644 --- a/src/gooddata_legacy2cloud/pp_dashboards/cloud_pp_dashboards_builder.py +++ b/src/gooddata_legacy2cloud/pp_dashboards/cloud_pp_dashboards_builder.py @@ -72,6 +72,11 @@ def __init__( "textItem", ] self.legacy_split_tabs = legacy_split_tabs + if self.ctx.keep_original_ids and self.legacy_split_tabs: + raise RuntimeError( + "`--keep-original-ids` cannot be used together with `--pp-legacy-split-tabs`. " + "Validate your config inputs." + ) self.cloud_dashboards: list[pdo.CloudDashboard] = [] self.public_dashboard_ids: list[str] = [] self.cloud_existing_dashboard_ids: list[str] = [] @@ -162,7 +167,8 @@ def _process_dashboard_as_tabbed( ) -> None: """Process a Legacy PP dashboard into one Cloud dashboard with native tabs.""" cloud_dashboard = pdo.CloudDashboard.create_tabbed_from_legacy_definition( - pixel_perfect_dashboard=legacy_dashboard + pixel_perfect_dashboard=legacy_dashboard, + keep_original_ids=self.ctx.keep_original_ids, ) # Write one-to-one mapping between Legacy and Cloud dashboards diff --git a/src/gooddata_legacy2cloud/pp_dashboards/data_classes.py b/src/gooddata_legacy2cloud/pp_dashboards/data_classes.py index b756938..3f67840 100644 --- a/src/gooddata_legacy2cloud/pp_dashboards/data_classes.py +++ b/src/gooddata_legacy2cloud/pp_dashboards/data_classes.py @@ -24,3 +24,4 @@ class PPDashboardContext: suppress_warnings: bool = field(default=False) client_prefix: str | None = field(default=None) exclude_tabs: list[str] | None = field(default=None) + keep_original_ids: bool = field(default=False) diff --git a/src/gooddata_legacy2cloud/reports/data_classes.py b/src/gooddata_legacy2cloud/reports/data_classes.py index eefb51c..16a3641 100644 --- a/src/gooddata_legacy2cloud/reports/data_classes.py +++ b/src/gooddata_legacy2cloud/reports/data_classes.py @@ -25,6 +25,7 @@ class ReportContext: mapping_logger: OutputWriter suppress_warnings: bool = field(default=False) client_prefix: str | None = field(default=None) + keep_original_ids: bool = field(default=False) # TODO: The ContextWithWarnings class should be untangled and removed. diff --git a/src/gooddata_legacy2cloud/reports/transformation.py b/src/gooddata_legacy2cloud/reports/transformation.py index 5565f59..2961cf8 100644 --- a/src/gooddata_legacy2cloud/reports/transformation.py +++ b/src/gooddata_legacy2cloud/reports/transformation.py @@ -79,13 +79,18 @@ def transform_legacy_report( legacy_title = meta.get("title", "Migrated Visualization") legacy_identifier = meta.get("identifier", "unknown") legacy_summary = meta.get("summary", "") - top_level_id = get_cloud_id(legacy_title, legacy_identifier) - # If this is a report migration, prepend the report insight prefix. - if meta.get("category") in ["report", "reportDefinition"]: - top_level_id = REPORT_INSIGHT_PREFIX + "__" + top_level_id - if REPORT_TITLE_PREFIX: # Only add prefix if it's not an empty string + if ctx.keep_original_ids: + top_level_id = legacy_identifier + if REPORT_TITLE_PREFIX: legacy_title = REPORT_TITLE_PREFIX + legacy_title + else: + top_level_id = get_cloud_id(legacy_title, legacy_identifier) + # If this is a report migration, prepend the report insight prefix. + if meta.get("category") in ["report", "reportDefinition"]: + top_level_id = REPORT_INSIGHT_PREFIX + "__" + top_level_id + if REPORT_TITLE_PREFIX: # Only add prefix if it's not an empty string + legacy_title = REPORT_TITLE_PREFIX + legacy_title # Log the mapping from Legacy to Cloud identifier ctx.mapping_logger.write_identifier_relation( diff --git a/src/gooddata_legacy2cloud/workflows/migrate_dashboards.py b/src/gooddata_legacy2cloud/workflows/migrate_dashboards.py index b531ec7..02be5d2 100644 --- a/src/gooddata_legacy2cloud/workflows/migrate_dashboards.py +++ b/src/gooddata_legacy2cloud/workflows/migrate_dashboards.py @@ -146,6 +146,7 @@ def migrate_dashboards(config: DashboardConfig): suppress_warnings=config.object_migration_config.suppress_migration_warnings, client_prefix=config.common_config.client_prefix, dashboard_type=config.dashboard_type, + keep_original_ids=config.keep_original_ids, ) logger.info("----Fetching Legacy dashboards----") diff --git a/src/gooddata_legacy2cloud/workflows/migrate_insights.py b/src/gooddata_legacy2cloud/workflows/migrate_insights.py index a93e3e1..95ccad5 100644 --- a/src/gooddata_legacy2cloud/workflows/migrate_insights.py +++ b/src/gooddata_legacy2cloud/workflows/migrate_insights.py @@ -119,6 +119,7 @@ def migrate_insights(config: InsightConfig): mapping_logger=mapping_logger, suppress_warnings=config.object_migration_config.suppress_migration_warnings, client_prefix=config.common_config.client_prefix, + keep_original_ids=config.keep_original_ids, ) logger.info("----Fetching Legacy insights----") diff --git a/src/gooddata_legacy2cloud/workflows/migrate_pixel_perfect_dashboards.py b/src/gooddata_legacy2cloud/workflows/migrate_pixel_perfect_dashboards.py index 0443a3f..3f79d63 100644 --- a/src/gooddata_legacy2cloud/workflows/migrate_pixel_perfect_dashboards.py +++ b/src/gooddata_legacy2cloud/workflows/migrate_pixel_perfect_dashboards.py @@ -172,6 +172,7 @@ def migrate_pixel_perfect_dashboards(config: PixelPerfectDashboardConfig) -> Non transformation_logger=transformation_logger, suppress_warnings=config.object_migration_config.suppress_migration_warnings, client_prefix=config.common_config.client_prefix, + keep_original_ids=config.keep_original_ids, ) cfg = GridConfig( diff --git a/src/gooddata_legacy2cloud/workflows/migrate_reports.py b/src/gooddata_legacy2cloud/workflows/migrate_reports.py index 06790e2..8ac84b7 100644 --- a/src/gooddata_legacy2cloud/workflows/migrate_reports.py +++ b/src/gooddata_legacy2cloud/workflows/migrate_reports.py @@ -121,6 +121,7 @@ def migrate_reports(config: ReportConfig): mapping_logger=mapping_logger, suppress_warnings=config.object_migration_config.suppress_migration_warnings, client_prefix=config.common_config.client_prefix, + keep_original_ids=config.keep_original_ids, ) logger.info("----Fetching Legacy reports----") diff --git a/tests/fixtures/dashboards_fixtures.py b/tests/fixtures/dashboards_fixtures.py index 46b291a..66f5509 100644 --- a/tests/fixtures/dashboards_fixtures.py +++ b/tests/fixtures/dashboards_fixtures.py @@ -89,6 +89,51 @@ def update_filter_context(filter_context_object): ) +@pytest.fixture +def dashboards_context_keep_ids( + legacy_client: LegacyClient, cloud_client: CloudClient, mocker +): + """Create DashboardContext with keep_original_ids=True for testing.""" + with open(f"{LEGACY_OBJECTS_DIR}/objects_by_uri.json", "r") as file: + objects_by_uri = json.load(file) + + def get_objects_by_uri(uri): + return objects_by_uri[uri] + + mocker.patch.object(legacy_client, "get_object", side_effect=get_objects_by_uri) + mocker.patch.object(cloud_client, "get_filter_context", return_value={}) + mocker.patch.object(cloud_client, "create_filter_context", return_value=None) + mocker.patch.object(cloud_client, "update_filter_context", return_value=None) + mocker.patch.object(cloud_client, "get_dashboards", return_value=[]) + mocker.patch.object(cloud_client, "get_insights", return_value=[]) + mocker.patch.object( + cloud_client, + "create_insight", + return_value=type("MockResponse", (), {"ok": True, "status_code": 201})(), + ) + + ldm_mappings = IdMappings(f"{MAPPING_FILES_DIR}/ldm_mappings.csv") + metric_mappings = IdMappings(f"{MAPPING_FILES_DIR}/metric_mappings.csv") + insight_mappings = IdMappings(f"{MAPPING_FILES_DIR}/insight_mappings.csv") + dashboard_mappings = IdMappings(f"{MAPPING_FILES_DIR}/dashboard_mappings.csv") + mapping_logger = mocker.MagicMock() + + return DashboardContext( + legacy_client=legacy_client, + cloud_client=cloud_client, + ldm_mappings=ldm_mappings, + metric_mappings=metric_mappings, + insight_mappings=insight_mappings, + mapping_logger=mapping_logger, + dashboard_mappings=dashboard_mappings, + suppress_warnings=False, + client_prefix=None, + current_batch_dashboard_mappings=None, + dashboard_type="analyticalDashboard", + keep_original_ids=True, + ) + + @pytest.fixture def dashboards_builder(dashboards_context): """Create CloudDashboardsBuilder instance.""" diff --git a/tests/fixtures/insights_fixtures.py b/tests/fixtures/insights_fixtures.py index 7bc5131..6bc5469 100644 --- a/tests/fixtures/insights_fixtures.py +++ b/tests/fixtures/insights_fixtures.py @@ -51,6 +51,37 @@ def get_objects_by_uri(uri): ) +@pytest.fixture +def insights_context_keep_ids( + legacy_client: LegacyClient, cloud_client: CloudClient, mocker +): + """Create InsightContext with keep_original_ids=True for testing.""" + with open(f"{LEGACY_OBJECTS_DIR}/objects_by_uri.json", "r") as file: + objects_by_uri = json.load(file) + + def get_objects_by_uri(uri): + return objects_by_uri[uri] + + mocker.patch.object(legacy_client, "get_object", side_effect=get_objects_by_uri) + mocker.patch.object(cloud_client, "get_attribute_json", return_value={}) + + ldm_mappings = IdMappings(f"{MAPPING_FILES_DIR}/ldm_mappings.csv") + metric_mappings = IdMappings(f"{MAPPING_FILES_DIR}/metric_mappings.csv") + mapping_logger = mocker.MagicMock() + + return InsightContext( + legacy_client=legacy_client, + cloud_client=cloud_client, + ldm_mappings=ldm_mappings, + metric_mappings=metric_mappings, + mapping_logger=mapping_logger, + report_mappings=None, + suppress_warnings=False, + client_prefix=None, + keep_original_ids=True, + ) + + @pytest.fixture def insights_builder(insights_context): """Create CloudInsightsBuilder instance.""" diff --git a/tests/fixtures/pp_dashboards_fixtures.py b/tests/fixtures/pp_dashboards_fixtures.py index 0eb4439..297d45e 100644 --- a/tests/fixtures/pp_dashboards_fixtures.py +++ b/tests/fixtures/pp_dashboards_fixtures.py @@ -10,6 +10,9 @@ from gooddata_legacy2cloud.backends.cloud.client import CloudClient from gooddata_legacy2cloud.backends.legacy.client import LegacyClient from gooddata_legacy2cloud.id_mappings import IdMappings +from gooddata_legacy2cloud.pp_dashboards.cloud_pp_dashboards_builder import ( + CloudPixelPerfectDashboardsBuilder, +) from gooddata_legacy2cloud.pp_dashboards.data_classes import PPDashboardContext from gooddata_legacy2cloud.pp_dashboards.grid_maker import GridConfig @@ -50,6 +53,28 @@ def pp_context(legacy_client: LegacyClient, cloud_client: CloudClient, mocker): ) +@pytest.fixture +def pp_context_keep_ids(legacy_client: LegacyClient, cloud_client: CloudClient, mocker): + """Mock pixel perfect context with keep_original_ids=True.""" + return PPDashboardContext( + legacy_client=legacy_client, + cloud_client=cloud_client, + ldm_mappings=IdMappings( + "tests/data/pixel_perfect_dashboards/mapping_files/ldm_mappings.csv" + ), + metric_mappings=IdMappings( + "tests/data/pixel_perfect_dashboards/mapping_files/metric_mappings.csv" + ), + report_mappings=IdMappings( + "tests/data/pixel_perfect_dashboards/mapping_files/report_mappings.csv" + ), + mapping_logger=mocker.MagicMock(), + suppress_warnings=False, + client_prefix=None, + keep_original_ids=True, + ) + + @pytest.fixture def mock_legacy_pp_dashboards(mocker, legacy_client: LegacyClient): """Mock Legacy API responses for pixel perfect dashboards.""" @@ -172,10 +197,6 @@ def mock_post(endpoint: str, data=None, headers=None): # noqa: ARG001 @pytest.fixture def pp_dashboards_builder(pp_context, pp_grid_config): """Create CloudPixelPerfectDashboardsBuilder instance for tests.""" - from gooddata_legacy2cloud.pp_dashboards.cloud_pp_dashboards_builder import ( - CloudPixelPerfectDashboardsBuilder, - ) - return CloudPixelPerfectDashboardsBuilder( ctx=pp_context, cfg=pp_grid_config, @@ -189,10 +210,6 @@ def pp_dashboards_builder(pp_context, pp_grid_config): @pytest.fixture def pp_dashboards_builder_legacy_split(pp_context, pp_grid_config): """Create builder instance configured for legacy split-tabs behavior.""" - from gooddata_legacy2cloud.pp_dashboards.cloud_pp_dashboards_builder import ( - CloudPixelPerfectDashboardsBuilder, - ) - return CloudPixelPerfectDashboardsBuilder( ctx=pp_context, cfg=pp_grid_config, diff --git a/tests/test_configuration_validation.py b/tests/test_configuration_validation.py index 1e96646..ed9501b 100644 --- a/tests/test_configuration_validation.py +++ b/tests/test_configuration_validation.py @@ -4,6 +4,7 @@ from gooddata_legacy2cloud.config.configuration_objects import ( MetricConfig, + PixelPerfectDashboardConfig, ScheduledExportConfig, WorkspaceConfig, is_client_prefix_used, @@ -116,3 +117,16 @@ def test_validate_config_side_effects_scheduled_export(): # Validation happened in model_post_init assert config.common_config.output_files_prefix == "test_" assert config.common_config.check_parent_workspace is True + + +def test_pp_config_keep_original_ids_and_split_tabs_conflict(): + """keep_original_ids and pp_legacy_split_tabs cannot be used together.""" + with pytest.raises(ValidationError, match="cannot be used together"): + PixelPerfectDashboardConfig( + workspace_config=WorkspaceConfig(), + common_config=CommonConfig(), + object_migration_config=ObjectMigrationConfig(), + object_filter_config=ObjectFilterConfig(), + keep_original_ids=True, + pp_legacy_split_tabs=True, + ) diff --git a/tests/test_dashboards.py b/tests/test_dashboards.py index 832f926..8216f3e 100644 --- a/tests/test_dashboards.py +++ b/tests/test_dashboards.py @@ -11,7 +11,6 @@ import pytest from pytest import CaptureFixture - from gooddata_legacy2cloud.dashboards.cloud_dashboard import CloudDashboard from gooddata_legacy2cloud.dashboards.cloud_dashboards_builder import ( CloudDashboardsBuilder, @@ -100,3 +99,24 @@ def test_dashboards_migration( f"stdout: {captured.out}\n" f"stderr: {captured.err}" ) + + +def test_dashboard_keep_original_ids( + dashboards_context_keep_ids, + mocker, +) -> None: + """When keep_original_ids=True, cloud ID equals the Legacy identifier.""" + mocker.patch.object(CloudDashboard, "_resolve_widget_type", return_value="insight") + + builder = CloudDashboardsBuilder(dashboards_context_keep_ids) + legacy_dashboards = load_json(f"{TEST_CASES_DIR}/basic_dashboard_legacy.json") + assert isinstance(legacy_dashboards, list), "Legacy dashboards should be a list" + builder.process_legacy_dashboards( + legacy_dashboards, skip_deploy=True, overwrite_existing=False + ) + cloud_dashboards = builder.get_cloud_dashboards() + + assert len(cloud_dashboards) == len(legacy_dashboards) + for cloud_dash, legacy_dash in zip(cloud_dashboards, legacy_dashboards): + legacy_id = legacy_dash["analyticalDashboard"]["meta"]["identifier"] + assert cloud_dash["data"]["id"] == legacy_id diff --git a/tests/test_insights.py b/tests/test_insights.py index 2f86443..9077255 100644 --- a/tests/test_insights.py +++ b/tests/test_insights.py @@ -10,7 +10,6 @@ """ import pytest - from gooddata_legacy2cloud.insights.cloud_insights_builder import CloudInsightsBuilder from tests.test_utils import dicts_are_equal, load_json @@ -71,3 +70,17 @@ def test_insights_migration( for actual, expected in zip(actual_sorted, expected_sorted): dicts_are_equal(actual, expected) dicts_are_equal(expected, actual) + + +def test_insight_keep_original_ids(insights_context_keep_ids) -> None: + """When keep_original_ids=True, cloud ID equals the Legacy identifier.""" + builder = CloudInsightsBuilder(insights_context_keep_ids) + legacy_insights = load_json(f"{TEST_CASES_DIR}/basic_insight_legacy.json") + assert isinstance(legacy_insights, list), "Insights should be a list" + builder.process_legacy_insights(legacy_insights) + cloud_insights = builder.get_cloud_insights() + + assert len(cloud_insights) == len(legacy_insights) + for cloud_insight, legacy_insight in zip(cloud_insights, legacy_insights): + legacy_id = legacy_insight["visualizationObject"]["meta"]["identifier"] + assert cloud_insight["data"]["id"] == legacy_id diff --git a/tests/test_pp_dashboards_migration.py b/tests/test_pp_dashboards_migration.py index f26f765..1a917f7 100644 --- a/tests/test_pp_dashboards_migration.py +++ b/tests/test_pp_dashboards_migration.py @@ -10,6 +10,9 @@ from pytest import CaptureFixture from gooddata_legacy2cloud.helpers import PP_FILTER_CONTEXT_PREFIX +from gooddata_legacy2cloud.pp_dashboards.cloud_pp_dashboards_builder import ( + CloudPixelPerfectDashboardsBuilder, +) from gooddata_legacy2cloud.pp_dashboards.utils import sanitize_string from tests.test_utils import load_json @@ -286,3 +289,29 @@ def test_tabbed_dashboard_all_tabs_unsupported_is_skipped( assert mock_cloud_pp_api.create_filter_context.call_count == 0 assert "Skipping dashboard" in caplog.text + + +def test_pp_dashboard_keep_original_ids( + pp_context_keep_ids, + pp_grid_config, + mock_legacy_pp_dashboards, + mock_cloud_pp_api, +) -> None: + """When keep_original_ids=True, cloud dashboard ID equals the Legacy PP identifier.""" + builder = CloudPixelPerfectDashboardsBuilder( + ctx=pp_context_keep_ids, + cfg=pp_grid_config, + pixel_perfect_prefix="[PP]", + min_text_length=5, + supported_items=["headlineItem", "reportItem", "textItem"], + legacy_split_tabs=False, + ) + builder.process_legacy_dashboards( + legacy_dashboards=[mock_legacy_pp_dashboards], + skip_deploy=True, + overwrite_existing=False, + ) + cloud_dashboards = builder.get_cloud_dashboards() + + assert len(cloud_dashboards) == 1 + assert cloud_dashboards[0].id == "test_pp_dashboard_001"