# Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. """Migrate iframe in dashboard to markdown component Revision ID: 978245563a02 Revises: f2672aa8350a Create Date: 2020-08-12 00:24:39.617899 """ import collections import json import logging import uuid from collections import defaultdict from alembic import op from sqlalchemy import and_, Column, ForeignKey, Integer, String, Table, Text from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import relationship from superset import db # revision identifiers, used by Alembic. revision = "978245563a02" down_revision = "f2672aa8350a" Base = declarative_base() class Slice(Base): """Declarative class to do query in upgrade""" __tablename__ = "slices" id = Column(Integer, primary_key=True) params = Column(Text) viz_type = Column(String(250)) dashboard_slices = Table( "dashboard_slices", Base.metadata, Column("id", Integer, primary_key=True), Column("dashboard_id", Integer, ForeignKey("dashboards.id")), Column("slice_id", Integer, ForeignKey("slices.id")), ) slice_user = Table( "slice_user", Base.metadata, Column("id", Integer, primary_key=True), Column("user_id", Integer, ForeignKey("ab_user.id")), Column("slice_id", Integer, ForeignKey("slices.id")), ) class Dashboard(Base): __tablename__ = "dashboards" id = Column(Integer, primary_key=True) position_json = Column(Text) slices = relationship("Slice", secondary=dashboard_slices, backref="dashboards") def create_new_markdown_component(chart_position, url): return { "type": "MARKDOWN", "id": "MARKDOWN-{}".format(uuid.uuid4().hex[:8]), "children": [], "parents": chart_position["parents"], "meta": { "width": chart_position["meta"]["width"], "height": chart_position["meta"]["height"], "code": f'', }, } def upgrade(): bind = op.get_bind() session = db.Session(bind=bind) dash_to_migrate = defaultdict(list) iframe_urls = defaultdict(list) try: # find iframe viz_type and its url iframes = session.query(Slice).filter_by(viz_type="iframe").all() iframe_ids = [slc.id for slc in iframes] for iframe in iframes: iframe_params = json.loads(iframe.params or "{}") url = iframe_params.get("url") iframe_urls[iframe.id] = url # find iframe viz_type that used in dashboard dash_slc = ( session.query(dashboard_slices) .filter(dashboard_slices.c.slice_id.in_(iframe_ids)) .all() ) for entry in dash_slc: dash_to_migrate[entry.dashboard_id].append(entry.slice_id) dashboard_ids = list(dash_to_migrate.keys()) # replace iframe in dashboard metadata dashboards = ( session.query(Dashboard).filter(Dashboard.id.in_(dashboard_ids)).all() ) for i, dashboard in enumerate(dashboards): print( f"scanning dashboard ({i + 1}/{len(dashboards)}) dashboard: {dashboard.id} >>>>" ) # remove iframe slices from dashboard dashboard.slices = [ slc for slc in dashboard.slices if slc.id not in iframe_ids ] # find iframe chart position in metadata # and replace it with markdown component position_dict = json.loads(dashboard.position_json or "{}") for key, chart_position in position_dict.items(): if ( chart_position and isinstance(chart_position, dict) and chart_position["type"] == "CHART" and chart_position["meta"] and chart_position["meta"]["chartId"] in iframe_ids ): iframe_id = chart_position["meta"]["chartId"] # make new markdown component markdown = create_new_markdown_component( chart_position, iframe_urls[iframe_id] ) position_dict.pop(key) position_dict[markdown["id"]] = markdown # add markdown to layout tree parent_id = markdown["parents"][-1] children = position_dict[parent_id]["children"] children.remove(key) children.append(markdown["id"]) dashboard.position_json = json.dumps( position_dict, indent=None, separators=(",", ":"), sort_keys=True, ) session.merge(dashboard) # remove iframe, separator and markup charts slices_to_remove = ( session.query(Slice) .filter(Slice.viz_type.in_(["iframe", "separator", "markup"])) .all() ) slices_ids = [slc.id for slc in slices_to_remove] # remove dependencies first session.query(dashboard_slices).filter( dashboard_slices.c.slice_id.in_(slices_ids) ).delete(synchronize_session=False) session.query(slice_user).filter(slice_user.c.slice_id.in_(slices_ids)).delete( synchronize_session=False ) # remove slices session.query(Slice).filter(Slice.id.in_(slices_ids)).delete( synchronize_session=False ) except Exception as ex: logging.exception(f"dashboard {dashboard.id} has error: {ex}") session.commit() session.close() def downgrade(): # note: this upgrade is irreversible. # this migration removed all iframe, separator, and markup type slices, # and Superset will not support these 3 viz_type anymore. pass