從前端角度記錄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> {% 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="width: 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> <