Skip to content
Open
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
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down
28 changes: 20 additions & 8 deletions src/gooddata_legacy2cloud/arg_parsing/arg_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand All @@ -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",
Expand Down Expand Up @@ -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()
Expand All @@ -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(
Expand Down
23 changes: 23 additions & 0 deletions src/gooddata_legacy2cloud/config/configuration_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)
Expand Down
25 changes: 15 additions & 10 deletions src/gooddata_legacy2cloud/dashboards/cloud_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
)
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/gooddata_legacy2cloud/dashboards/data_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
9 changes: 6 additions & 3 deletions src/gooddata_legacy2cloud/insights/cloud_insight.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
1 change: 1 addition & 0 deletions src/gooddata_legacy2cloud/insights/data_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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=[])),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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] = []
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/gooddata_legacy2cloud/pp_dashboards/data_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
1 change: 1 addition & 0 deletions src/gooddata_legacy2cloud/reports/data_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 10 additions & 5 deletions src/gooddata_legacy2cloud/reports/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions src/gooddata_legacy2cloud/workflows/migrate_dashboards.py
Original file line number Diff line number Diff line change
Expand Up @@ -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----")
Expand Down
1 change: 1 addition & 0 deletions src/gooddata_legacy2cloud/workflows/migrate_insights.py
Original file line number Diff line number Diff line change
Expand Up @@ -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----")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions src/gooddata_legacy2cloud/workflows/migrate_reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -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----")
Expand Down
Loading
Loading