Skip to content
Merged
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
47 changes: 0 additions & 47 deletions ddpui/api/dashboard_native_api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Native Dashboard API endpoints"""

import copy
from typing import Optional, List
from datetime import timedelta

Expand Down Expand Up @@ -174,15 +173,12 @@ def duplicate_dashboard(request, dashboard_id: int):

# Create a copy of the dashboard
with transaction.atomic():
# First create new dashboard WITHOUT layout_config and components (we'll update them later)
new_dashboard = Dashboard.objects.create(
title=f"Copy of {original_dashboard.title}",
description=original_dashboard.description,
dashboard_type=original_dashboard.dashboard_type,
grid_columns=original_dashboard.grid_columns,
target_screen_size=original_dashboard.target_screen_size,
layout_config=[], # Will be updated after filter duplication
components={}, # Will be updated after filter duplication
created_by=orguser,
org=orguser.org,
last_modified_by=orguser,
Expand All @@ -204,47 +200,6 @@ def duplicate_dashboard(request, dashboard_id: int):
)
filter_id_mapping[str(original_filter.id)] = str(new_filter.id)

# Now update layout_config and components with new filter IDs

# Deep copy the original data to avoid modifying it
new_layout_config = copy.deepcopy(original_dashboard.layout_config or [])
new_components = copy.deepcopy(original_dashboard.components or {})

# Update layout_config: change component IDs from "filter-{old_id}" to "filter-{new_id}"
for layout_item in new_layout_config:
item_id = layout_item.get("i", "")
if item_id.startswith("filter-"):
# Extract old filter ID and replace with new one
old_filter_id = item_id.replace("filter-", "")
if old_filter_id in filter_id_mapping:
new_filter_id = filter_id_mapping[old_filter_id]
layout_item["i"] = f"filter-{new_filter_id}"

# Update components: change component keys and filterId references
updated_components = {}
for component_id, component_data in new_components.items():
new_component_id = component_id
new_component_data = copy.deepcopy(component_data)

# If this is a filter component
if component_id.startswith("filter-"):
old_filter_id = component_id.replace("filter-", "")
if old_filter_id in filter_id_mapping:
new_filter_id = filter_id_mapping[old_filter_id]
new_component_id = f"filter-{new_filter_id}"

# Update the filterId reference in the component config
if (
"config" in new_component_data
and "filterId" in new_component_data["config"]
):
new_component_data["config"]["filterId"] = int(new_filter_id)

updated_components[new_component_id] = new_component_data

# Update the dashboard with the corrected layout_config, components, and tabs
new_dashboard.layout_config = new_layout_config
new_dashboard.components = updated_components
new_dashboard.tabs = DashboardService.copy_tabs_with_filter_remapping(
original_dashboard.tabs or [], filter_id_mapping
)
Expand Down Expand Up @@ -422,8 +377,6 @@ def get_filter_options(
raise HttpError(400, "No warehouse configured for organization")

# Get filter options from service
from ddpui.services.dashboard_service import DashboardService

options = DashboardService.generate_filter_options(
schema=schema_name,
table=table_name,
Expand Down
10 changes: 1 addition & 9 deletions ddpui/core/reports/report_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,14 @@ def _freeze_dashboard(dashboard: Dashboard) -> Dict[str, Any]:
"description": dashboard.description,
"grid_columns": dashboard.grid_columns,
"target_screen_size": dashboard.target_screen_size,
"layout_config": dashboard.layout_config,
"components": dashboard.components,
"tabs": dashboard.tabs,
"filter_layout": dashboard.filter_layout,
"filters": [f.to_json() for f in filters],
}

@staticmethod
def _extract_chart_ids(dashboard: Dashboard) -> List[int]:
"""Extract chart IDs from tabs (new structure) and root components (backward compat)."""
"""Extract chart IDs from tabs."""
chart_ids = []

for tab in dashboard.tabs or []:
Expand All @@ -81,12 +79,6 @@ def _extract_chart_ids(dashboard: Dashboard) -> List[int]:
if chart_id:
chart_ids.append(chart_id)

for component in (dashboard.components or {}).values():
if component.get("type") == "chart":
chart_id = component.get("config", {}).get("chartId")
if chart_id:
chart_ids.append(chart_id)

return list(set(chart_ids))

@staticmethod
Expand Down
86 changes: 86 additions & 0 deletions ddpui/management/commands/cleanup_frozen_dashboard_root_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from django.core.management.base import BaseCommand
from ddpui.models.report import ReportSnapshot


class Command(BaseCommand):
"""
Remove stale root-level layout_config and components keys from
ReportSnapshot.frozen_dashboard JSON blobs.

These keys were left in place after the initial tabs migration for rollback
safety. All data now lives inside frozen_dashboard.tabs — the root-level
keys are no longer read and can be removed.
"""

help = "Remove root-level layout_config and components from frozen_dashboard JSON"

def add_arguments(self, parser):
"""Add --dry-run argument to preview changes without applying them."""
parser.add_argument(
"--dry-run",
action="store_true",
help="Preview changes without applying them",
)

def handle(self, *args, **options):
"""Execute the cleanup, iterating all ReportSnapshot records in batches."""
dry_run = options["dry_run"]

if dry_run:
self.stdout.write("\n=== DRY RUN MODE: No changes will be made ===\n")

cleaned_count = 0
skipped_count = 0

snapshots_to_update = []

for snapshot in ReportSnapshot.objects.only("id", "frozen_dashboard").iterator(
chunk_size=1000
):
frozen = snapshot.frozen_dashboard
if not isinstance(frozen, dict):
skipped_count += 1
continue

has_layout = "layout_config" in frozen
has_components = "components" in frozen

if not has_layout and not has_components:
skipped_count += 1
continue

cleaned_count += 1

if dry_run:
self.stdout.write(
f"[DRY RUN] Would clean - Snapshot ID: {snapshot.id}, "
f"keys to remove: {[k for k in ('layout_config', 'components') if k in frozen]}"
)
else:
frozen.pop("layout_config", None)
frozen.pop("components", None)
snapshot.frozen_dashboard = frozen
snapshots_to_update.append(snapshot)

if not dry_run and snapshots_to_update:
ReportSnapshot.objects.bulk_update(
snapshots_to_update, ["frozen_dashboard"], batch_size=500
)
for snapshot in snapshots_to_update:
self.stdout.write(f"Cleaned - Snapshot ID: {snapshot.id}")

self.stdout.write("")
if dry_run:
self.stdout.write(
self.style.WARNING(
f"=== DRY RUN COMPLETE: {cleaned_count} snapshots would be cleaned, "
f"{skipped_count} skipped ==="
)
)
else:
self.stdout.write(
self.style.SUCCESS(
f"=== CLEANUP COMPLETE: {cleaned_count} snapshots cleaned, "
f"{skipped_count} skipped ==="
)
)
95 changes: 0 additions & 95 deletions ddpui/management/commands/migrate_dashboards_to_tabs.py

This file was deleted.

Loading
Loading