From eb04aa4831626e4d13d071a4ec36a8d737052c5f Mon Sep 17 00:00:00 2001 From: Grace Date: Wed, 12 Aug 2020 00:33:43 -0700 Subject: [PATCH] refactor: [migration] convert iframe chart into dashboard markdown component --- .../src/dashboard/reducers/getInitialState.js | 4 +- .../src/explore/components/SaveModal.jsx | 6 +- .../components/controls/VizTypeControl.jsx | 3 - superset-frontend/src/explore/constants.js | 2 +- ...5563a02_migrate_iframe_to_dash_markdown.py | 164 ++++++++++++++++++ 5 files changed, 173 insertions(+), 6 deletions(-) create mode 100644 superset/migrations/versions/978245563a02_migrate_iframe_to_dash_markdown.py diff --git a/superset-frontend/src/dashboard/reducers/getInitialState.js b/superset-frontend/src/dashboard/reducers/getInitialState.js index 770e24963dc95..8aa0bb3c207ff 100644 --- a/superset-frontend/src/dashboard/reducers/getInitialState.js +++ b/superset-frontend/src/dashboard/reducers/getInitialState.js @@ -108,7 +108,9 @@ export default function (bootstrapData) { const sliceIds = new Set(); dashboard.slices.forEach(slice => { const key = slice.slice_id; - if (['separator', 'markup'].indexOf(slice.form_data.viz_type) === -1) { + if ( + ['separator', 'markup', 'iframe'].indexOf(slice.form_data.viz_type) === -1 + ) { const form_data = { ...slice.form_data, url_params: { diff --git a/superset-frontend/src/explore/components/SaveModal.jsx b/superset-frontend/src/explore/components/SaveModal.jsx index 45b96ffd5160d..c97d4133cb086 100644 --- a/superset-frontend/src/explore/components/SaveModal.jsx +++ b/superset-frontend/src/explore/components/SaveModal.jsx @@ -134,6 +134,7 @@ class SaveModal extends React.Component { this.setState({ alert: null }); } render() { + // do not add iframe/markdown/separator chart to dashboard const canNotSaveToDash = EXPLORE_ONLY_VIZ_TYPE.indexOf(this.state.vizType) > -1; return ( @@ -239,7 +240,10 @@ class SaveModal extends React.Component { bsSize="sm" bsStyle="primary" onClick={this.saveOrOverwrite.bind(this, false)} - disabled={!this.state.newSliceName} + disabled={ + !this.state.newSliceName || + (canNotSaveToDash && this.state.newDashboardName) + } > {t('Save')} diff --git a/superset-frontend/src/explore/components/controls/VizTypeControl.jsx b/superset-frontend/src/explore/components/controls/VizTypeControl.jsx index 2c67f6837aab1..cf5ad648f3ed5 100644 --- a/superset-frontend/src/explore/components/controls/VizTypeControl.jsx +++ b/superset-frontend/src/explore/components/controls/VizTypeControl.jsx @@ -74,7 +74,6 @@ const DEFAULT_ORDER = [ 'line_multi', 'treemap', 'box_plot', - 'separator', 'sunburst', 'sankey', 'word_cloud', @@ -85,7 +84,6 @@ const DEFAULT_ORDER = [ 'bubble', 'deck_geojson', 'horizon', - 'markup', 'deck_multi', 'compare', 'partition', @@ -95,7 +93,6 @@ const DEFAULT_ORDER = [ 'world_map', 'paired_ttest', 'para', - 'iframe', 'country_map', ]; diff --git a/superset-frontend/src/explore/constants.js b/superset-frontend/src/explore/constants.js index 9ef5c9ed74675..1bf3cc8056577 100644 --- a/superset-frontend/src/explore/constants.js +++ b/superset-frontend/src/explore/constants.js @@ -71,7 +71,7 @@ export const sqlaAutoGeneratedMetricNameRegex = /^(sum|min|max|avg|count|count_d export const sqlaAutoGeneratedMetricRegex = /^(LONG|DOUBLE|FLOAT)?(SUM|AVG|MAX|MIN|COUNT)\([A-Z0-9_."]*\)$/i; export const druidAutoGeneratedMetricRegex = /^(LONG|DOUBLE|FLOAT)?(SUM|MAX|MIN|COUNT)\([A-Z0-9_."]*\)$/i; -export const EXPLORE_ONLY_VIZ_TYPE = ['separator', 'markup']; +export const EXPLORE_ONLY_VIZ_TYPE = ['separator', 'markup', 'iframe']; export const TIME_FILTER_LABELS = { time_range: t('Time Range'), diff --git a/superset/migrations/versions/978245563a02_migrate_iframe_to_dash_markdown.py b/superset/migrations/versions/978245563a02_migrate_iframe_to_dash_markdown.py new file mode 100644 index 0000000000000..e927d2489b082 --- /dev/null +++ b/superset/migrations/versions/978245563a02_migrate_iframe_to_dash_markdown.py @@ -0,0 +1,164 @@ +# 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")), +) + + +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, value in position_dict.items(): + if ( + value + and isinstance(value, dict) + and value["type"] == "CHART" + and value["meta"] + and value["meta"]["chartId"] in iframe_ids + ): + iframe_id = value["meta"]["chartId"] + # make new markdown component + markdown = create_new_markdown_component( + value, 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) + except Exception as ex: + logging.exception(f"dashboard {dashboard.id} has error: {ex}") + + session.commit() + session.close() + + +def downgrade(): + pass