• 从前端角度记录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=" 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/

  • 相关阅读:
    Python-cookie,session
    Django_models下划线__正反查询,对象正反查询
    Python利用PIL生成随机验证码图片
    简单实用的分页类-python
    Django_Form验证(三)字段,字段的参数,widgets种类
    Django_Form验证(二),ajax验证
    Django_Form验证(一)
    Django提交文件的方式
    在linux下安装python3.6.6
    celery学习
  • 原文地址:https://www.cnblogs.com/hao987/p/16078325.html
Copyright © 2020-2023  润新知