项目里 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> {% 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_link
的href
属性对应,这里的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代码写的,写两套的用意大概是向开发者展示两种写法,我们可以使用其中一种,如果两种都用了, 在修改菜单时需要注意两处都要修改:
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> {% 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=" 18px; text-align: center;"></i> {% 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>