从前端角度记录superset二次开发(转载)

项目里 superset 版本是 0.36.0, python 版本是 3.6, 网上大部分资料都是后端开发人员贡献的,这篇文章我从一个前端的角度记录一下 superset 二次开发遇到的一些问题和解决方法。

先讲一下项目的大概结构:

  • 整个项目的后台代码使用了 python,这部分放在项目根目录的 superset 目录下
  • 一整个后台的框架页面使用了 jinjia2,在项目根目录/superset/templates 下查看
  • 页面上图表相关的展示和操作使用了 react,在/superset-frontend 目录下查看
  • 前端打包后的页面放在/superset/static 目录下

1. 修改/添加生成图表的表单项

src/explore目录中都是生成图表的表单项相关的代码,如果想添加一项只用在src/explore/controls.jsx文件中,模拟controls对象中的一项去添加一个属性,如添加一个’all_columns_x’

all_columns_x: {
    type: 'SelectControl', //可以在explore/components/controls目录中找到对应的组件
    label: 'X',
    default: null,
    description: t('Columns to display'),
    mapStateToProps: state => ({
      choices: columnChoices(state.datasource),
    }),
  },

2. 修改透视表(Pivot Table)中的默认排序列

3. 透视表表头和表内容列错位

4. 修改默认语言为中文

修改superset/config.py文件

BABEL_DEFAULT_LOCALE = "zh"

5. 添加新菜单、菜单跳转到新页面

找到 navbar_menu.html

添加菜单涉及到权限,需要后台开发人员配合,可以先避开权限问题,让添加的菜单显示出来,完成前端部分工作。

打开 superset/templates/appbuilder/navbar_menu.html 文件,如果 appbuilder 下没有 navbar_menu.html,可以到本地安装的 superset 目录下找,比如我的 superset 装在/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/superset/下,那就可以到这个目录找 templates/appbuilder/navbar_menu.html,在修改过程中遇到项目中没有的 html 文件都是这样操作,找到后如果需要修改这个 html 文件,可以把它复制到自己项目的对应文件夹下。

修改 navbar_menu.html 文件

is_menu_visible是用来过滤菜单的,先把它注释掉

{% for item1 in menu.get_list() %}
<!-- is_menu_visible -->
{% if item1 | is_menu_visible %} {% if item1.childs %}
<li class="dropdown">
  <a class="dropdown-toggle" data-toggle="dropdown" href="javascript:void(0)" target="_blank" rel="noopener">
    {% if item1.icon %}
    <i class="fa {{item1.icon}}"></i>&nbsp; {% endif %} {{_(item1.label)}}<b class="caret"></b></a>
  <ul class="dropdown-menu">
    {% for item2 in item1.childs %} {% if item2 %} {% if item2.name == '-' %} {%
    if not loop.last %}
    <li class="divider"></li>
    {% endif %}
    <!-- | is_menu_visible  -->
    {% elif item2 | is_menu_visible %}
    <li>{{ menu_item(item2) }}</li>
    {% endif %} {% endif %} {% endfor %}
  </ul>
</li>
{% else %}
<li>{{ menu_item(item1) }}</li>
{% endif %} {% endif %} {% endfor %}
</ul></li>
添加菜单

修改 superset/app.py 文件

appbuilder.add_link(
    "New Menu",
    label=__("New Menu"),
    href="/superset/new",
    icon="fa-cloud-upload",
    category="New",
    category_label=__("New"),
    category_icon="fa-wrench",
)
添加处理函数

修改 superset/views/core.py 文件, 在class Superset下添加

@has_access
@expose("/new", methods=["GET", "POST"])
def doudizhu_events(self):
    """SQL Editor"""
    bootstrap_data = json.dumps({})
    return self.render_template(
        "superset/basic.html", entry="new", bootstrap_data=bootstrap_data
    )

class Superset中定义的处理函数根路径都是/superset,所以现在就有了一个/superset/new的路径,与上面add_linkhref属性对应,这里的entry="new"指向的是 react 的入口文件

添加入口文件

修改/superset-frontend/webpack.config.js文件,在 config.entry 下添加新的入口

entry: {
  theme: path.join(APP_DIR, "/src/new/index.jsx");
}

在对应的/src/new下添加index.jsx文件,可以仿照superset-frontend/src/addSlice下的文件

index.js

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

ReactDOM.render(<App />, document.getElementById("app"));

App.jsx

import React from "react";
import { hot } from "react-hot-loader/root";
import setupApp from "../../setup/setupApp";
import setupPlugins from "../../setup/setupPlugins";
import New from "./New";

setupApp();
setupPlugins();

const appContainer = document.getElementById("app");
const bootstrapData = JSON.parse(appContainer.getAttribute("data-bootstrap"));

const App = () => <New datasources={bootstrapData.datasources} />;

export default hot(App);
编写组件代码

New.jsx 中就是正常的 react 组件代码

6. 添加新图例,引入 echarts

参考

以添加一个简单的折线图为例

  • 在 superset-frontend/src/visualizations/ 目录下新建文件夹 SimpleLine,在 SimpleLine 文件夹下新建 images 文件夹,images 文件夹中放 SimpleLine 这个新图例的的缩略图,然后继续在 SimpleLine 文件夹下新建 SimpleLine.jsx,SimpleLinePlugin.js,transformProps.js,
新建 SimpleLinePlugin.js
import { t } from "@superset-ui/translation";
import { ChartMetadata, ChartPlugin } from "@superset-ui/chart";
import transformProps from "./transformProps";
import thumbnail from "./images/thumbnail.png";

const metadata = new ChartMetadata({
  name: t("Simple Line"),
  description: "",
  thumbnail,
});

export default class SimpleLinePlugin extends ChartPlugin {
  constructor() {
    super({
      metadata,
      transformProps,
      loadChart: () => import("./SimpleLine.jsx"),
    });
  }
}
新建 transformProps.js

这个文件单纯的用来转换数据,可以在这里把从后端接收到的数据处理成前端展示需要的格式

export default function transformProps(chartProps) {
  const {
    height,
    width,
    datasource,
    formData,
    queryData,
    rawFormData,
  } = chartProps;
  const { records, columns } = queryData.data;

  return {
    width,
    height,
    data: records,
    columns: columns,
    columns_x: rawFormData.all_columns_x,
    columns_y: rawFormData.all_columns_y,
  };
}
新建 SimpleLine.jsx

这部分代码我只放了个大概,主要做的工作就是通过 props 接收参数,然后导入echarts-for-react并使用,关于 echarts 的配置,直接参考 echarts 文档。

import React from "react";
import PropTypes from "prop-types";
import ReactEcharts from "echarts-for-react";

const propTypes = {
  data: PropTypes.array,
  columns: PropTypes.columns,
  width: PropTypes.number,
  height: PropTypes.number,
  columns_x: PropTypes.string,
  columns_y: PropTypes.string,
}; //检查类型,其中data包含viz.py中返回的数据,width和height为图表宽高

class SimpleLine extends React.PureComponent {
  render() {
    const options = {
      xAxis: {
        type: "category",
        data: [],
      },
      yAxis: {
        type: "value",
      },
      series: [
        {
          name: yName,
          data: [],
          type: "line",
        },
      ],
    };
    return (
      <ReactEcharts
        option={options}
        style={{ height: this.props.height }}
      ></ReactEcharts>
    );
  }
}

SimpleLine.displayName = "simple line";
SimpleLine.propTypes = propTypes;

export default SimpleLine;
修改文件/superset-frontend/src/setup/setupPlugins.ts
// 文件开头导入SimpleLine
import SimpleLine from '../explore/controlPanels/SimpleLine';

//注册SimpleLine,在getChartControlPanelRegistry()方法的链式调用后追加一句
.registerValue('simple_line', SimpleLine)
修改文件/superset-frontend/src/visualizations/presets/MainPreset.js
//导入
import SimpleLineChartPlugin from "../SimpleLine/SimpleLinePlugin";

//在plugins后添加
new SimpleLineChartPlugin().configure({ key: "simple_line" });
后端代码添加 class SimpleLine

修改/superset/viz.py文件,在viz_types的定义前添加class SimpleLine,下面这段代码根据你需要的数据自行进行处理,这里只做最简单的演示

class SimpleLine(BaseViz):
    viz_type = 'simple_line'
    verbose_name = "simple line"
    sort_series = False
    is_timeseries = False
    def query_obj(self):
        d = super().query_obj()
        fd = self.form_data #form_data中包含界面左侧组件内容
        columns = []
        if not fd.get('all_columns'): #这个字段对应×××组件,不为空
            raise Exception('Choose Columns')
        if fd.get('all_columns'):
            d['columns'] = columns # all_columns是左侧组件名,后面会提到
        return d

    def get_data(self, df):
        # df是pandas的DataFrame类型
        data = np.array(df).tolist() #假设数据很简单,不需要做别的处理
        # 如果除了绘图用的数据还有别的信息,可以构造一个字典来返回
        # data = {'plot_data':plot_data,'other_info':other_info}

        return self.handle_js_int_overflow(
            dict(records=df.to_dict(orient="records"), columns=list(df.columns))
        )

这样就大功告成了。

7. 三级菜单

菜单的修改都需要注意,登录成功后进入的welcome页面和其他页面使用的模板不一样,welcome页面的菜单是通过react代码写的,写两套的用意大概是向开发者展示两种写法,我们可以使用其中一种,如果两种都用了, 在修改菜单时需要注意两处都要修改:

  1. superset/templates/appbuilder/navbar_menu.html
    {% for item1 in menu.get_list() %}
     {% if item1 | is_menu_visible %}
         {% if item1.childs %}
             <li class="dropdown">
             <a class="dropdown-toggle" data-toggle="dropdown" href="javascript:void(0)" target="_blank" rel="noopener">
             {% if item1.icon %}
                 <i class="fa {{item1.icon}}"></i>&nbsp;
             {% endif %}
             {{_(item1.label)}}<b class="caret"></b></a>
             <ul class="dropdown-menu">
             {% for item2 in item1.childs %}
                 {% if item2 %}
                     {% if item2.childs %}
                         <li class="dropdown-submenu" style="position:relative">
                             <a class="dropdown-toggle" data-toggle="dropdown" href="javascript:void(0)" target="_blank" rel="noopener">
                             {% if item2.icon %}
                                 <i class="fa {{item2.icon}}" style="width: 18px; text-align: center;"></i>&nbsp;
                             {% endif %}
                             {{_(item2.label)}}<b class="fa fa-chevron-right" style="margin-left:10px"></b></a>
                             <ul class="dropdown-menu" style="left: 100%;top: -3px;">
                                 {% for item3 in item2.childs %}
                                     {% if item3 %}
                                         {% if item3.name == '-' %}
                                             {% if not loop.last %}
                                                 <li class="divider"></li>
                                             {% endif %}
                                             {% elif item3 %}
                                                 <li>{{ menu_item(item3) }}</li>
                                         {% endif %}
                                     {% endif %}
                                 {% endfor %}
                          </ul>
                      </li>
                  {% else %}
                      {% if item2.name == '-' %}
                             {% if not loop.last %}
                             <li class="divider"></li>
                             {% endif %}
                      {% elif item2 | is_menu_visible %}
                          <li>{{ menu_item(item2) }}</li>
                      {% endif %}
                  {% endif %}
              {% endif %}
          {% endfor %}
          </ul></li>
      {% else %}
          <li>
              {{ menu_item(item1) }}
          </li>
      {% endif %}
    {% endif %}
    {% endfor %}
    </ul></li></ul></li>
  2. <NavDropdown
    id={`menu-dropdown-${label}`}
    eventKey={index}
    title={navTitle}
    className={className}
    >
    {childs.map((child, index1) =>
     //新添加,递归多级菜单
     child.childs && child.childs.length > 0 ? (
       <MenuObject {...child} className="right_menu_wrap" />
     ) : child === '-' ? (
       <MenuItem key={`$${index1}`} divider />
     ) : (
       <MenuItem
         key={`${child.label}`}
         href={child.url}
         eventKey={parseFloat(`${index}.${index1}`)}
       >
         <i className={`fa ${child.icon}`} />
         &nbsp; {child.label}
       </MenuItem>
     ),
    )}
    </NavDropdown>
    
    添加js和css
    修改文件superset/templates/appbuilder/baselayout.html
    <script type="text/javascript">
    $(document).ready(function () {
     $('.dropdown-submenu').hover(function () {
       $(this).children('ul').show()
     }, function () {
       $(this).children('ul').hide()
     })
    });
    </script>
    
    修改文件superset/templates/superset/basic.html
    <style>
    .dropdown-submenu {
     position: relative;
    }
    .dropdown-submenu:hover>.dropdown-menu {
     display: block;
     left: 100%;
     top: 0;
    }
    .fa {
     width: 18px;
     text-align: center;
    }
    </style>
    

    8. 修改看板tab的选项卡最大数量

    目录:superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx,修改MAX_TAB_COUNT

9. 扩展看板能保存的最长字符

修改superset/config.py下的SUPERSET_DASHBOARD_POSITION_DATA_LIMIT字段

9. 解决在看板中快速切换筛选条件导致的数据错乱(自定义图例)

解决方法:

  1. 中断请求
  2. 记录上一次请求的时间,并将时间作为参数传到后台,请求成功后,后台返回的数据中返回请求时传入的时间,对比两次时间,如果不一致,丢弃请求结果

10. 修改翻译文件后更新看不到效果

翻译文件修改后需要编译,将.po文件编译为.mo文件才会生效

cd ~/superset/superset/translations/zh/LC_MESSAGES

msgfmt ./messages.po -o ./messages.mo

11. 修改页面默认筛选条件

举个例子,比如数据表页面,默认的显示的筛选条件是表名/以开始,但最常用的是表名/包含,想让页面默认筛选条件显示表名/包含,只用修改配置文件中链接地址,在superset/app.py文件下找到指定页面的菜单修改:

appbuilder.add_link(
    "Tables",
    label=__("Tables"),
    href="/tablemodelview/list/?_flt_2_table_name",
    icon="fa-table",
    category="Sources",
    category_label=__("Sources"),
    category_icon="fa-table",
)

这里主要修改_flt_2_table_name,其中的数字表示的是筛选条件下拉列表中选项的下标,其中包含的下标是2,所以改为_flt_2_table_name

 

转载自: http://sunjl729.cn/2020/08/07/superset%E4%BA%8C%E6%AC%A1%E5%BC%80%E5%8F%91/

posted @ 2022-03-30 17:59  我是一颗小菜  阅读(4386)  评论(1编辑  收藏  举报