1. 程式人生 > 其它 >從前端角度記錄superset二次開發(轉載)

從前端角度記錄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>
                      <