Superset embed Dashboard到React App

1,154 阅读5分钟

一、环境

  • Python 3.8.0
  • Linux version 3.10.0-1160.53.1.el7.x86_64
  • node v14.19.0
  • superset1.5

二、操作步骤

2.1 创建react app 项目

npx

npx create-react-app my-app

npm

npm init react-app my-app

Yarn

yarn create react-app my-app

参考文档:Create React App 中文文档 Getting Started

2.2 下载依赖包

// 核心包,用于将superset嵌入到第三方app
npm install @superset-ui/embedded-sdk
// http库,用于发送http请求
npm install axios
// webpack5中移除了nodejs核心模块的polyfill自动引入,所以需要手动引入。
//用于解决 @superset-ui/embedded-sdk中Buffer的相关报错
npm install node-polyfill-webpack-plugin
// 对项目中 wepback 进行自定义配置
npm install @craco/craco
// 地址代理
npm install http-proxy-middleware

2.3 修改相关配置信息

2.3.1 修改package.json

我们用craco修改webpack配置,并用craco启动项目

"scripts": {
- "start": "react-scripts start",
- "build": "react-scripts build",
- "test": "react-scripts test",
+ "start": "craco start",
+ "build": "craco build",
+ "test": "craco test",
+ "eject": "craco eject"
}

2.3.2 在根目录下新建craco.config.js,并添加以下代码

const webpack = require('webpack');

const NodePolyfillPlugin = require('node-polyfill-webpack-plugin')

module.exports = {
  configureWebpack: config => {
    const plugins = []
    // 手动引入node
    plugins.push(new NodePolyfillPlugin())
  },
  webpack: {
    plugins: [
      // 手动引入node环境下的Buffer
      new webpack.ProvidePlugin({
        Buffer: ["buffer", "Buffer"]
      })
    ]
  },
  devServer: {
    host: 'xxxx', // 当前主机地址
  } }

2.3.3 在src目录下新建setupProxy.js,并配置http请求代理地址

 const {createProxyMiddleware: proxy} = require('http-proxy-middleware');

module.exports = function(app){
    app.use(         proxy('/superset',{
            target:'xxxx', // http请求代理地址
            changeOrigin:true,
            pathRewrite:{'^/superset':''}
        })
    )
}

2.4 启动项目

cd my-app
npm start

2.5 新增请求相关文件

2.5.1 在src下新建request文件夹,里面创建三个文件request.js、api.js、type.js

2.5.2 request.js 用于处理基本请求的公共方法

//request.js import axios from "axios";
/** **** 创建axios实例 ***** */
const service = axios.create();

/** **** request拦截器==>对请求参数做处理 ***** */
service.interceptors.request.use(config => {
  const url = process.env.NODE_ENV === 'production' ? window.ENV_CONFIG.HTTP_SERVER : '/superset';
    config.url = `${url}${config.url}`;
    return config;
}, error => { //请求错误处理    const err = error || '服务器内部错误';     Promise.reject(err)
});

/** **** respone拦截器==>对响应做处理 ***** */
service.interceptors.response.use(
    response => { //成功请求到数据
    // 可自行扩展       if(response.data.message){
        return Promise.reject(response.data.message);
      }
      //这里根据后端提供的数据进行对应的处理        return Promise.resolve(response.data);
    },
    error => { //响应错误处理      console.log('error');
      const err = error || '服务器内部错误';       return Promise.reject(err)
    }
  );
  export default service;

2.5.3 type.js 用于存放请求的地址

该项目中有两个请求地址,一个是获取用户token的地址,一个是获取guest_token的地址,地址如下:

export const LOGIN_URL = '/api/v1/security/login';
export const GUEST_TOKEN_URL = '/api/v1/security/guest_token/';

2.5.4 api.js 用于存放各类请求

该项目中有两个请求方法,一个是获取用户的token,一个是获取guest_token,方法如下

//api.js import {LOGIN_URL, GUEST_TOKEN_URL} from './type';
import service from './base';
// 获取用户token export const getUserToken = (data) => {
  return service({
      url: LOGIN_URL,
      method: 'post',
      data
  });
}

// 获取guest_token export const getGuestToken = (data, headers) => {
  return service({
      url: GUEST_TOKEN_URL,
      method: 'post',
      data,
      headers
  });
}

2.6 编写fetchGuestTokenFromBackend方法

在src文件夹下新建embeded.js, 代码如下:


import {getUserToken, getGuestToken} from './request/api';

export default async function fetchGuestTokenFromBackend(config) {
    const {username, password, provider = 'ldap', refresh = true, dashboardId, userId} = config
    // 获取token    const { access_token } = await getUserToken({
            username,
            password,
            provider,
            refresh
    });
    const { token } = await getGuestToken({
            "resources": [
              {
                "id": dashboardId,
                "type": "dashboard"
              }
            ],
            "rls": [],
            "user": {
              "first_name": username,
              "last_name": username,
              "username": username
            }
        },
        {
            Authorization: `Bearer ${access_token}`
        },
    )
    return token;
}

2.7 开启embed dashboard权限

2.7.1 superset项目中 /superset/config.py,查找EMBED相关权限,将EMBEDDED_SUPERSET改为True

"EMBEDDED_SUPERSET": True,

此时看板功能可以出现Embed dashboard

2.7.2 获取embedId,将地址加入白名单

选择要嵌入的看板,点击Embed dashboard 弹出以下窗口

填入开发地址(地址+端口号),获取ID

2.8 在App.js中编写embeded方法


import React from 'react';
import { embedDashboard } from "@superset-ui/embedded-sdk";
import fetchGuestTokenFromBackend from './embeded';
import './App.css'

const App = () => {
    let json = {
      // superset中已加入的用户
      username: 'test',
      password: '123456',
      // 要插入iframe的dom id       domId: "superset-container",
      provider: 'db',
      // 需要在supertset中进行配置,同2.1.6中生成的id       dashboardId: 'xxx',
      // superset运行地址       supersetDomain: 'xxx'
    };
    fetchGuestTokenFromBackend(json).then((token) => {
      embedDashboard({ 
        id: json.dashboardId, // given by the Superset embedding UI        supersetDomain: json.supersetDomain,
        mountPoint: document.getElementById((json)?.domId), // html element in which iframe render        fetchGuestToken: () => token, 
        dashboardUiConfig: { hideTitle: true } 
      });
      
    });
  // 与dom id相同
  return <div id="superset-container"></div>
};
export default App;

此时是无法访问superset看板的,需要后台赋予相应的权限

2.9 给embed用户赋权

2.9.1 新建角色test_role

此时可以做以下操作

选择角色,点击操作,复制角色,可以出现下面角色,此时可以修改成test_role

2.9.2 修改xx/superset/config.py

查找GUEST_ROLE_NAME,给它赋予test_role权限(与2.9.1创建的角色相同)

GUEST_ROLE_NAME = "test_role"

此时在react app中还是无法浏览看板,需要有获取guest_token的权限

2.9.3 新建test_role_grant角色,给角色赋予以下权限

2.9.4 将test_role_grant赋权给test用户

此时embed功能已完成,效果如下

参考: www.npmjs.com/package/@su…

三、如何适应手机端

3.1 查找图表实现相关组件

3.1.1 DashboardGrid.jsx 看板网格布局组件

查看元素标签,我们可以发现整个看板都是在grid-container里的,其对应入口superset-frontend/src/dashboard/components/DashboardGrid.jsx

3.1.2 DashboardComponent.jsx 根绝类型渲染不同组件的中间方法

继续向下查找,发现一行就是一个grid-row,其对应入口superset-frontend/src/dashboard/containers/DashboardComponent.jsx

3.1.3 superset-frontend/src/dashboard/components/gridComponents/index.js 所有组成看板组件的集合

根据DashboardComponent组件的方法查找componentLookup

发现componentLookup:可以根据组件类型生成对应的组件

此时可以在DashboardComponent中打印component相关信息

发现component.type = CHART时会渲染chart相关组件

3.1.4 superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx

在ChartHolder中,可以发现chart的宽度是由chartWidth控制的

查找chartWidth相关定义,发现可以自动计算

因此可以通过修改width的相关计算方式来实现手机端上一行一个图表的效果。

3.2 修改chartWidth相关计算方法

在superset-frontend/src/dashboard/components/gridComponents/ChartHolder.jsx中添加以下代码,当窗口宽度<700并且chart的容器宽度>100的时候做相应的计算

if (isFullSize) {
      chartWidth = window.innerWidth - CHART_MARGIN;
      chartHeight = window.innerHeight - CHART_MARGIN;
    //  TODO: 平铺     } else if(window.innerWidth < 700 && document.querySelector('.slice_container')?.clientWidth > 100) {
      chartWidth = document.querySelector('.slice_container')?.clientWidth;
      chartHeight = Math.floor(
        component.meta.height * GRID_BASE_UNIT - CHART_MARGIN,
      );
    } else {
      chartWidth = Math.floor(
        widthMultiple * columnWidth +
          (widthMultiple - 1) * GRID_GUTTER_SIZE -
          CHART_MARGIN,
      );
      chartHeight = Math.floor(
        component.meta.height * GRID_BASE_UNIT - CHART_MARGIN,
      );
    }

3.3 修改相关css

找到superset-frontend/src/dashboard/stylesheets/components/row.less,添加一下代码,强制更改容器的宽度,以达到平铺的效果

@media screen and (max-width:700px) {
  .grid-container {
    margin-left: 10px !important;
    margin-right: 10px !important;
  }
  .grid-row {
    flex-wrap: wrap;
    .dragdroppable-column {
      width: 100%;
      margin-right: 0 !important;
      margin-bottom: 20px;
      &:last-child {
        margin-bottom: 0;
      }
      .resizable-container {
        width: 100% !important;
        max-width: 100% !important;       }
    }
  }
}

最终效果

四、bug及解决方式

4.1 [Violation] Added non-passive event listener to a scroll-blocking <some> event. Consider marking eve

embed后手机模式下会出现该警告,导致页面不停闪烁

解决方式:

4.1.1 安装 default-passive-events

npm i default-passive-events 

4.1.2 全局引入

import 'default-passive-events';

4.2 embed后页面白屏

检查代码中supersetDomain地址是否正确,尾部不能有斜杠(/)

4.3 superset-embedded-sdk: Buffer is not defined

当 webpack >= 5时需要在配置文件里手动引入相关核心模块,即Polyfill Node.js核心模块, Buffer

解决:在craco.config.js里添加以下代码

const path = require('path');
const webpack = require('webpack');

const NodePolyfillPlugin = require('node-polyfill-webpack-plugin')

module.exports = {
  configureWebpack: config => {
    const plugins = []
    plugins.push(new NodePolyfillPlugin())
  },
  webpack: {
    plugins: [
      new webpack.ProvidePlugin({
        Buffer: ["buffer", "Buffer"]
      })
    ]
  },
}