2020
2121from flask_babel import lazy_gettext as _
2222
23- from superset import security_manager
23+ from superset import db , security_manager
2424from superset .commands .base import BaseCommand
2525from superset .commands .chart .exceptions import (
2626 ChartDeleteFailedError ,
3131from superset .daos .chart import ChartDAO
3232from superset .daos .report import ReportScheduleDAO
3333from superset .exceptions import SupersetSecurityException
34+ from superset .models .dashboard import Dashboard
3435from superset .models .slice import Slice
36+ from superset .utils import json
3537from superset .utils .decorators import on_error , transaction
3638
3739logger = logging .getLogger (__name__ )
@@ -46,6 +48,9 @@ def __init__(self, model_ids: list[int]):
4648 def run (self ) -> None :
4749 self .validate ()
4850 assert self ._models
51+ # Clean up dashboard metadata before deleting charts
52+ for chart in self ._models :
53+ self ._cleanup_dashboard_metadata (chart .id )
4954 ChartDAO .delete (self ._models )
5055
5156 def validate (self ) -> None :
@@ -68,3 +73,132 @@ def validate(self) -> None:
6873 security_manager .raise_for_ownership (model )
6974 except SupersetSecurityException as ex :
7075 raise ChartForbiddenError () from ex
76+
77+ def _cleanup_dashboard_metadata ( # noqa: C901
78+ self , chart_id : int
79+ ) -> None :
80+ """
81+ Remove references to this chart from all dashboard metadata.
82+
83+ When a chart is deleted, dashboards may still contain references to the
84+ chart ID in various metadata fields (expanded_slices, filter_scopes, etc.).
85+ This method cleans up those references to prevent issues during dashboard
86+ export/import.
87+ """
88+ # Find all dashboards that contain this chart
89+ dashboards = (
90+ db .session .query (Dashboard )
91+ .filter (Dashboard .slices .any (id = chart_id )) # type: ignore[attr-defined]
92+ .all ()
93+ )
94+
95+ for dashboard in dashboards :
96+ metadata = dashboard .params_dict
97+ modified = False
98+
99+ # Clean up expanded_slices
100+ if "expanded_slices" in metadata :
101+ chart_id_str = str (chart_id )
102+ if chart_id_str in metadata ["expanded_slices" ]:
103+ del metadata ["expanded_slices" ][chart_id_str ]
104+ modified = True
105+ logger .info (
106+ "Removed chart %s from expanded_slices in dashboard %s" ,
107+ chart_id ,
108+ dashboard .id ,
109+ )
110+
111+ # Clean up timed_refresh_immune_slices
112+ if "timed_refresh_immune_slices" in metadata :
113+ if chart_id in metadata ["timed_refresh_immune_slices" ]:
114+ metadata ["timed_refresh_immune_slices" ].remove (chart_id )
115+ modified = True
116+ logger .info (
117+ "Removed chart %s from timed_refresh_immune_slices "
118+ "in dashboard %s" ,
119+ chart_id ,
120+ dashboard .id ,
121+ )
122+
123+ # Clean up filter_scopes
124+ if "filter_scopes" in metadata :
125+ chart_id_str = str (chart_id )
126+ if chart_id_str in metadata ["filter_scopes" ]:
127+ del metadata ["filter_scopes" ][chart_id_str ]
128+ modified = True
129+ logger .info (
130+ "Removed chart %s from filter_scopes in dashboard %s" ,
131+ chart_id ,
132+ dashboard .id ,
133+ )
134+ # Also clean from immune lists
135+ for filter_scope in metadata ["filter_scopes" ].values ():
136+ for attributes in filter_scope .values ():
137+ if chart_id in attributes .get ("immune" , []):
138+ attributes ["immune" ].remove (chart_id )
139+ modified = True
140+
141+ # Clean up default_filters
142+ if "default_filters" in metadata :
143+ default_filters = json .loads (metadata ["default_filters" ])
144+ chart_id_str = str (chart_id )
145+ if chart_id_str in default_filters :
146+ del default_filters [chart_id_str ]
147+ metadata ["default_filters" ] = json .dumps (default_filters )
148+ modified = True
149+ logger .info (
150+ "Removed chart %s from default_filters in dashboard %s" ,
151+ chart_id ,
152+ dashboard .id ,
153+ )
154+
155+ # Clean up native_filter_configuration scope exclusions
156+ if "native_filter_configuration" in metadata :
157+ for native_filter in metadata ["native_filter_configuration" ]:
158+ scope_excluded = native_filter .get ("scope" , {}).get ("excluded" , [])
159+ if chart_id in scope_excluded :
160+ scope_excluded .remove (chart_id )
161+ modified = True
162+ logger .info (
163+ "Removed chart %s from native_filter_configuration "
164+ "in dashboard %s" ,
165+ chart_id ,
166+ dashboard .id ,
167+ )
168+
169+ # Clean up chart_configuration
170+ if "chart_configuration" in metadata :
171+ chart_id_str = str (chart_id )
172+ if chart_id_str in metadata ["chart_configuration" ]:
173+ del metadata ["chart_configuration" ][chart_id_str ]
174+ modified = True
175+ logger .info (
176+ "Removed chart %s from chart_configuration in dashboard %s" ,
177+ chart_id ,
178+ dashboard .id ,
179+ )
180+
181+ # Clean up global_chart_configuration scope exclusions
182+ if "global_chart_configuration" in metadata :
183+ scope_excluded = (
184+ metadata ["global_chart_configuration" ]
185+ .get ("scope" , {})
186+ .get ("excluded" , [])
187+ )
188+ if chart_id in scope_excluded :
189+ scope_excluded .remove (chart_id )
190+ modified = True
191+ logger .info (
192+ "Removed chart %s from global_chart_configuration "
193+ "in dashboard %s" ,
194+ chart_id ,
195+ dashboard .id ,
196+ )
197+
198+ if modified :
199+ dashboard .json_metadata = json .dumps (metadata )
200+ logger .info (
201+ "Cleaned up metadata for dashboard %s after deleting chart %s" ,
202+ dashboard .id ,
203+ chart_id ,
204+ )
0 commit comments