Skip to content

Commit fd947a0

Browse files
asher-labAsher Manangan
and
Asher Manangan
authored
feat(tags): Export and Import Functionality for Superset Dashboards and Charts (#30833)
Co-authored-by: Asher Manangan <[email protected]>
1 parent e1383d3 commit fd947a0

File tree

18 files changed

+512
-18
lines changed

18 files changed

+512
-18
lines changed

Diff for: docker/.env

-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ MAPBOX_API_KEY=''
6262

6363
# Make sure you set this to a unique secure random value on production
6464
SUPERSET_SECRET_KEY=TEST_NON_DEV_SECRET
65-
6665
ENABLE_PLAYWRIGHT=false
6766
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
6867
BUILD_SUPERSET_FRONTEND_IN_DOCKER=true

Diff for: superset/charts/schemas.py

+1
Original file line numberDiff line numberDiff line change
@@ -1564,6 +1564,7 @@ class ImportV1ChartSchema(Schema):
15641564
dataset_uuid = fields.UUID(required=True)
15651565
is_managed_externally = fields.Boolean(allow_none=True, dump_default=False)
15661566
external_url = fields.String(allow_none=True)
1567+
tags = fields.List(fields.String(), allow_none=True)
15671568

15681569

15691570
class ChartCacheWarmUpRequestSchema(Schema):

Diff for: superset/commands/chart/export.py

+26
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,13 @@
2626
from superset.daos.chart import ChartDAO
2727
from superset.commands.dataset.export import ExportDatasetsCommand
2828
from superset.commands.export.models import ExportModelsCommand
29+
from superset.commands.tag.export import ExportTagsCommand
2930
from superset.models.slice import Slice
31+
from superset.tags.models import TagType
3032
from superset.utils.dict_import_export import EXPORT_VERSION
3133
from superset.utils.file import get_filename
3234
from superset.utils import json
35+
from superset.extensions import feature_flag_manager
3336

3437
logger = logging.getLogger(__name__)
3538

@@ -71,9 +74,23 @@ def _file_content(model: Slice) -> str:
7174
if model.table:
7275
payload["dataset_uuid"] = str(model.table.uuid)
7376

77+
# Fetch tags from the database if TAGGING_SYSTEM is enabled
78+
if feature_flag_manager.is_feature_enabled("TAGGING_SYSTEM"):
79+
tags = getattr(model, "tags", [])
80+
payload["tags"] = [tag.name for tag in tags if tag.type == TagType.custom]
7481
file_content = yaml.safe_dump(payload, sort_keys=False)
7582
return file_content
7683

84+
_include_tags: bool = True # Default to True
85+
86+
@classmethod
87+
def disable_tag_export(cls) -> None:
88+
cls._include_tags = False
89+
90+
@classmethod
91+
def enable_tag_export(cls) -> None:
92+
cls._include_tags = True
93+
7794
@staticmethod
7895
def _export(
7996
model: Slice, export_related: bool = True
@@ -85,3 +102,12 @@ def _export(
85102

86103
if model.table and export_related:
87104
yield from ExportDatasetsCommand([model.table.id]).run()
105+
106+
# Check if the calling class is ExportDashboardCommands
107+
if (
108+
export_related
109+
and ExportChartsCommand._include_tags
110+
and feature_flag_manager.is_feature_enabled("TAGGING_SYSTEM")
111+
):
112+
chart_id = model.id
113+
yield from ExportTagsCommand().export(chart_ids=[chart_id])

Diff for: superset/commands/chart/importers/v1/__init__.py

+20-2
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,27 @@
1414
# KIND, either express or implied. See the License for the
1515
# specific language governing permissions and limitations
1616
# under the License.
17+
from __future__ import annotations
1718

1819
from typing import Any
1920

2021
from marshmallow import Schema
2122
from sqlalchemy.orm import Session # noqa: F401
2223

24+
from superset import db
2325
from superset.charts.schemas import ImportV1ChartSchema
2426
from superset.commands.chart.exceptions import ChartImportError
2527
from superset.commands.chart.importers.v1.utils import import_chart
2628
from superset.commands.database.importers.v1.utils import import_database
2729
from superset.commands.dataset.importers.v1.utils import import_dataset
2830
from superset.commands.importers.v1 import ImportModelsCommand
31+
from superset.commands.importers.v1.utils import import_tag
2932
from superset.commands.utils import update_chart_config_dataset
3033
from superset.connectors.sqla.models import SqlaTable
3134
from superset.daos.chart import ChartDAO
3235
from superset.databases.schemas import ImportV1DatabaseSchema
3336
from superset.datasets.schemas import ImportV1DatasetSchema
37+
from superset.extensions import feature_flag_manager
3438

3539

3640
class ImportChartsCommand(ImportModelsCommand):
@@ -47,7 +51,13 @@ class ImportChartsCommand(ImportModelsCommand):
4751
import_error = ChartImportError
4852

4953
@staticmethod
50-
def _import(configs: dict[str, Any], overwrite: bool = False) -> None: # noqa: C901
54+
# ruff: noqa: C901
55+
def _import(
56+
configs: dict[str, Any],
57+
overwrite: bool = False,
58+
contents: dict[str, Any] | None = None,
59+
) -> None:
60+
contents = {} if contents is None else contents
5161
# discover datasets associated with charts
5262
dataset_uuids: set[str] = set()
5363
for file_name, config in configs.items():
@@ -93,4 +103,12 @@ def _import(configs: dict[str, Any], overwrite: bool = False) -> None: # noqa:
93103
"datasource_name": dataset.table_name,
94104
}
95105
config = update_chart_config_dataset(config, dataset_dict)
96-
import_chart(config, overwrite=overwrite)
106+
chart = import_chart(config, overwrite=overwrite)
107+
108+
# Handle tags using import_tag function
109+
if feature_flag_manager.is_feature_enabled("TAGGING_SYSTEM"):
110+
if "tags" in config:
111+
target_tag_names = config["tags"]
112+
import_tag(
113+
target_tag_names, contents, chart.id, "chart", db.session
114+
)

Diff for: superset/commands/dashboard/export.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import yaml
2626

2727
from superset.commands.chart.export import ExportChartsCommand
28+
from superset.commands.tag.export import ExportTagsCommand
2829
from superset.commands.dashboard.exceptions import DashboardNotFoundError
2930
from superset.commands.dashboard.importers.v1.utils import find_chart_uuids
3031
from superset.daos.dashboard import DashboardDAO
@@ -33,9 +34,11 @@
3334
from superset.daos.dataset import DatasetDAO
3435
from superset.models.dashboard import Dashboard
3536
from superset.models.slice import Slice
37+
from superset.tags.models import TagType
3638
from superset.utils.dict_import_export import EXPORT_VERSION
3739
from superset.utils.file import get_filename
3840
from superset.utils import json
41+
from superset.extensions import feature_flag_manager # Import the feature flag manager
3942

4043
logger = logging.getLogger(__name__)
4144

@@ -112,6 +115,7 @@ def _file_name(model: Dashboard) -> str:
112115
return f"dashboards/{file_name}.yaml"
113116

114117
@staticmethod
118+
# ruff: noqa: C901
115119
def _file_content(model: Dashboard) -> str:
116120
payload = model.export_to_dict(
117121
recursive=False,
@@ -159,10 +163,16 @@ def _file_content(model: Dashboard) -> str:
159163

160164
payload["version"] = EXPORT_VERSION
161165

166+
# Check if the TAGGING_SYSTEM feature is enabled
167+
if feature_flag_manager.is_feature_enabled("TAGGING_SYSTEM"):
168+
tags = model.tags if hasattr(model, "tags") else []
169+
payload["tags"] = [tag.name for tag in tags if tag.type == TagType.custom]
170+
162171
file_content = yaml.safe_dump(payload, sort_keys=False)
163172
return file_content
164173

165174
@staticmethod
175+
# ruff: noqa: C901
166176
def _export(
167177
model: Dashboard, export_related: bool = True
168178
) -> Iterator[tuple[str, Callable[[], str]]]:
@@ -173,7 +183,15 @@ def _export(
173183

174184
if export_related:
175185
chart_ids = [chart.id for chart in model.slices]
176-
yield from ExportChartsCommand(chart_ids).run()
186+
dashboard_ids = model.id
187+
command = ExportChartsCommand(chart_ids)
188+
command.disable_tag_export()
189+
yield from command.run()
190+
command.enable_tag_export()
191+
if feature_flag_manager.is_feature_enabled("TAGGING_SYSTEM"):
192+
yield from ExportTagsCommand.export(
193+
dashboard_ids=dashboard_ids, chart_ids=chart_ids
194+
)
177195

178196
payload = model.export_to_dict(
179197
recursive=False,

Diff for: superset/commands/dashboard/importers/v1/__init__.py

+32-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
# specific language governing permissions and limitations
1616
# under the License.
1717

18+
from __future__ import annotations
19+
1820
from typing import Any
1921

2022
from marshmallow import Schema
@@ -34,11 +36,13 @@
3436
from superset.commands.database.importers.v1.utils import import_database
3537
from superset.commands.dataset.importers.v1.utils import import_dataset
3638
from superset.commands.importers.v1 import ImportModelsCommand
39+
from superset.commands.importers.v1.utils import import_tag
3740
from superset.commands.utils import update_chart_config_dataset
3841
from superset.daos.dashboard import DashboardDAO
3942
from superset.dashboards.schemas import ImportV1DashboardSchema
4043
from superset.databases.schemas import ImportV1DatabaseSchema
4144
from superset.datasets.schemas import ImportV1DatasetSchema
45+
from superset.extensions import feature_flag_manager
4246
from superset.migrations.shared.native_filters import migrate_dashboard
4347
from superset.models.dashboard import Dashboard, dashboard_slices
4448

@@ -58,9 +62,15 @@ class ImportDashboardsCommand(ImportModelsCommand):
5862
import_error = DashboardImportError
5963

6064
# TODO (betodealmeida): refactor to use code from other commands
61-
# pylint: disable=too-many-branches, too-many-locals
65+
# pylint: disable=too-many-branches, too-many-locals, too-many-statements
6266
@staticmethod
63-
def _import(configs: dict[str, Any], overwrite: bool = False) -> None: # noqa: C901
67+
# ruff: noqa: C901
68+
def _import(
69+
configs: dict[str, Any],
70+
overwrite: bool = False,
71+
contents: dict[str, Any] | None = None,
72+
) -> None:
73+
contents = {} if contents is None else contents
6474
# discover charts and datasets associated with dashboards
6575
chart_uuids: set[str] = set()
6676
dataset_uuids: set[str] = set()
@@ -120,6 +130,14 @@ def _import(configs: dict[str, Any], overwrite: bool = False) -> None: # noqa:
120130
charts.append(chart)
121131
chart_ids[str(chart.uuid)] = chart.id
122132

133+
# Handle tags using import_tag function
134+
if feature_flag_manager.is_feature_enabled("TAGGING_SYSTEM"):
135+
if "tags" in config:
136+
target_tag_names = config["tags"]
137+
import_tag(
138+
target_tag_names, contents, chart.id, "chart", db.session
139+
)
140+
123141
# store the existing relationship between dashboards and charts
124142
existing_relationships = db.session.execute(
125143
select([dashboard_slices.c.dashboard_id, dashboard_slices.c.slice_id])
@@ -140,6 +158,18 @@ def _import(configs: dict[str, Any], overwrite: bool = False) -> None: # noqa:
140158
if (dashboard.id, chart_id) not in existing_relationships:
141159
dashboard_chart_ids.append((dashboard.id, chart_id))
142160

161+
# Handle tags using import_tag function
162+
if feature_flag_manager.is_feature_enabled("TAGGING_SYSTEM"):
163+
if "tags" in config:
164+
target_tag_names = config["tags"]
165+
import_tag(
166+
target_tag_names,
167+
contents,
168+
dashboard.id,
169+
"dashboard",
170+
db.session,
171+
)
172+
143173
# set ref in the dashboard_slices table
144174
values = [
145175
{"dashboard_id": dashboard_id, "slice_id": chart_id}

Diff for: superset/commands/database/importers/v1/__init__.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
# specific language governing permissions and limitations
1616
# under the License.
1717

18+
from __future__ import annotations
19+
1820
from typing import Any
1921

2022
from marshmallow import Schema
@@ -42,7 +44,11 @@ class ImportDatabasesCommand(ImportModelsCommand):
4244
import_error = DatabaseImportError
4345

4446
@staticmethod
45-
def _import(configs: dict[str, Any], overwrite: bool = False) -> None:
47+
def _import(
48+
configs: dict[str, Any],
49+
overwrite: bool = False,
50+
contents: dict[str, Any] | None = None,
51+
) -> None:
4652
# first import databases
4753
database_ids: dict[str, int] = {}
4854
for file_name, config in configs.items():

Diff for: superset/commands/dataset/importers/v1/__init__.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
# specific language governing permissions and limitations
1616
# under the License.
1717

18-
from typing import Any
18+
from typing import Any, Optional
1919

2020
from marshmallow import Schema
2121
from sqlalchemy.orm import Session # noqa: F401
@@ -42,7 +42,13 @@ class ImportDatasetsCommand(ImportModelsCommand):
4242
import_error = DatasetImportError
4343

4444
@staticmethod
45-
def _import(configs: dict[str, Any], overwrite: bool = False) -> None:
45+
def _import(
46+
configs: dict[str, Any],
47+
overwrite: bool = False,
48+
contents: Optional[dict[str, Any]] = None,
49+
) -> None:
50+
if contents is None:
51+
contents = {}
4652
# discover databases associated with datasets
4753
database_uuids: set[str] = set()
4854
for file_name, config in configs.items():

Diff for: superset/commands/export/assets.py

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ def run(self) -> Iterator[tuple[str, Callable[[], str]]]:
5353
ExportDashboardsCommand,
5454
ExportSavedQueriesCommand,
5555
]
56+
5657
for command in commands:
5758
ids = [model.id for model in command.dao.find_all()]
5859
for file_name, file_content in command(ids, export_related=False).run():

Diff for: superset/commands/importers/v1/__init__.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@
1414
# KIND, either express or implied. See the License for the
1515
# specific language governing permissions and limitations
1616
# under the License.
17+
18+
from __future__ import annotations
19+
1720
import logging
18-
from typing import Any, Optional
21+
from typing import Any
1922

2023
from marshmallow import Schema, validate # noqa: F401
2124
from marshmallow.exceptions import ValidationError
@@ -64,7 +67,12 @@ def __init__(self, contents: dict[str, str], *args: Any, **kwargs: Any):
6467
self._configs: dict[str, Any] = {}
6568

6669
@staticmethod
67-
def _import(configs: dict[str, Any], overwrite: bool = False) -> None:
70+
# ruff: noqa: C901
71+
def _import(
72+
configs: dict[str, Any],
73+
overwrite: bool = False,
74+
contents: dict[str, Any] | None = None,
75+
) -> None:
6876
raise NotImplementedError("Subclasses MUST implement _import")
6977

7078
@classmethod
@@ -76,7 +84,7 @@ def run(self) -> None:
7684
self.validate()
7785

7886
try:
79-
self._import(self._configs, self.overwrite)
87+
self._import(self._configs, self.overwrite, self.contents)
8088
except CommandException:
8189
raise
8290
except Exception as ex:
@@ -87,7 +95,7 @@ def validate(self) -> None: # noqa: F811
8795

8896
# verify that the metadata file is present and valid
8997
try:
90-
metadata: Optional[dict[str, str]] = load_metadata(self.contents)
98+
metadata: dict[str, str] | None = load_metadata(self.contents)
9199
except ValidationError as exc:
92100
exceptions.append(exc)
93101
metadata = None

Diff for: superset/commands/importers/v1/examples.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
# KIND, either express or implied. See the License for the
1515
# specific language governing permissions and limitations
1616
# under the License.
17-
from typing import Any
17+
from typing import Any, Optional
1818

1919
from marshmallow import Schema
2020
from sqlalchemy.exc import MultipleResultsFound
@@ -90,6 +90,7 @@ def _get_uuids(cls) -> set[str]:
9090
def _import( # pylint: disable=too-many-locals, too-many-branches # noqa: C901
9191
configs: dict[str, Any],
9292
overwrite: bool = False,
93+
contents: Optional[dict[str, Any]] = None,
9394
force_data: bool = False,
9495
) -> None:
9596
# import databases

0 commit comments

Comments
 (0)