-
Notifications
You must be signed in to change notification settings - Fork 99
feat: add multi-tab support to dashboards #1283
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
d7dd1cd
6b5f3c1
e085886
0b58853
a2ea8da
1df7062
7df5ba8
addf465
b863474
694f221
bf9f52d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| import time | ||
| from django.core.management.base import BaseCommand | ||
| from ddpui.models.dashboard import Dashboard | ||
|
|
||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| class Command(BaseCommand): | ||
| """ | ||
| Migrate existing dashboards to use tabs structure. | ||
| Moves layout_config and components from root level into a default tab. | ||
| """ | ||
|
|
||
| help = "Migrate existing dashboards to tabs structure for all orgs" | ||
|
|
||
| def add_arguments(self, parser): | ||
| parser.add_argument( | ||
| "--dry-run", | ||
| action="store_true", | ||
| help="Preview changes without applying them", | ||
| ) | ||
|
|
||
| def handle(self, *args, **options): | ||
| dry_run = options["dry_run"] | ||
|
|
||
| if dry_run: | ||
| self.stdout.write("\n=== DRY RUN MODE: No changes will be made ===\n") | ||
|
|
||
| migrated_count = 0 | ||
| skipped_count = 0 | ||
|
|
||
| for dashboard in Dashboard.objects.only( | ||
| "id", "title", "tabs", "layout_config", "components", "org" | ||
| ).iterator(chunk_size=1000): | ||
| # Skip if already has tabs | ||
| if dashboard.tabs and len(dashboard.tabs) > 0: | ||
| skipped_count += 1 | ||
| continue | ||
|
|
||
| # Skip if no data to migrate | ||
| has_layout = dashboard.layout_config and len(dashboard.layout_config) > 0 | ||
| has_components = dashboard.components and len(dashboard.components) > 0 | ||
|
|
||
| if not has_layout and not has_components: | ||
| skipped_count += 1 | ||
| continue | ||
|
|
||
| # This dashboard needs migration | ||
| migrated_count += 1 | ||
| org_id = getattr(dashboard, "org_id", "N/A") | ||
|
|
||
| if dry_run: | ||
| self.stdout.write( | ||
| f"[DRY RUN] Would migrate - " | ||
| f"Dashboard ID: {dashboard.id}, " | ||
| f"Title: {dashboard.title}, " | ||
| f"Org ID: {org_id}, " | ||
| f"Charts: {len(dashboard.layout_config or [])}" | ||
| ) | ||
| else: | ||
| # Create default tab with existing data | ||
| default_tab = { | ||
| "id": f"tab-{int(time.time() * 1000)}", | ||
| "title": "Untitled Tab 1", | ||
| "layout_config": dashboard.layout_config or [], | ||
| "components": dashboard.components or {}, | ||
| } | ||
|
|
||
| # Update dashboard structure | ||
| dashboard.tabs = [default_tab] | ||
| dashboard.layout_config = [] | ||
| dashboard.components = {} | ||
| dashboard.save(update_fields=["tabs", "layout_config", "components"]) | ||
|
|
||
| self.stdout.write( | ||
| f"Migrated - " | ||
| f"Dashboard ID: {dashboard.id}, " | ||
| f"Title: {dashboard.title}, " | ||
| f"Org ID: {org_id}, " | ||
| f"Charts: {len(default_tab['layout_config'])}" | ||
| ) | ||
|
|
||
| self.stdout.write("") | ||
| if dry_run: | ||
| self.stdout.write( | ||
| self.style.WARNING( | ||
| f"=== DRY RUN COMPLETE: {migrated_count} dashboards would be migrated, " | ||
| f"{skipped_count} skipped ===" | ||
| ) | ||
| ) | ||
| else: | ||
| self.stdout.write( | ||
| self.style.SUCCESS( | ||
| f"=== MIGRATION COMPLETE: {migrated_count} dashboards migrated, " | ||
| f"{skipped_count} skipped ===" | ||
| ) | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| # Generated by Django 4.2 on 2026-03-25 20:32 | ||
|
|
||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
| dependencies = [ | ||
| ("ddpui", "0156_add_comment_snapshot_index"), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AddField( | ||
| model_name="dashboard", | ||
| name="tabs", | ||
| field=models.JSONField( | ||
| default=list, | ||
| help_text="Array of tab objects: [{id, title, layout_config, components}]", | ||
| ), | ||
| ), | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ | |
| from datetime import datetime, timedelta | ||
| from dataclasses import dataclass | ||
| import json | ||
| import time | ||
| import uuid | ||
|
|
||
| from django.core.cache import cache | ||
|
|
@@ -360,10 +361,19 @@ def create_dashboard(data: DashboardData, orguser: OrgUser) -> Dashboard: | |
| Returns: | ||
| Created Dashboard instance | ||
| """ | ||
| # Generate default tab for new dashboard | ||
| default_tab = { | ||
| "id": f"tab-{int(time.time() * 1000)}", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we really need this? dashboard id will be different right?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, the Dashboard id is different from the tab id. |
||
| "title": "Untitled Tab 1", | ||
| "layout_config": [], | ||
| "components": {}, | ||
| } | ||
|
|
||
| dashboard = Dashboard.objects.create( | ||
| title=data.title, | ||
| description=data.description, | ||
| grid_columns=data.grid_columns, | ||
| tabs=[default_tab], # Initialize with default tab | ||
| created_by=orguser, | ||
| org=orguser.org, | ||
| last_modified_by=orguser, | ||
|
|
@@ -414,6 +424,9 @@ def update_dashboard( | |
| dashboard.layout_config = data.layout_config | ||
| if data.components is not None: | ||
| dashboard.components = data.components | ||
| if data.tabs is not None: | ||
| # Convert Pydantic models to dicts for JSON storage | ||
| dashboard.tabs = [tab.dict() for tab in data.tabs] | ||
| if data.filter_layout is not None: | ||
| dashboard.filter_layout = data.filter_layout | ||
| if data.is_published is not None: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I dont understand this. once migration is done isn't this will be just dead code?
The migration command clears root-level layout_config/components and moves everything into tabs, but the frontend save still sends both . Can we make
tabs the single source of truth and stop using root-level fields for tabbed dashboards