[BI] 自助BI看板集成 - 支持Superset和Metabase

- 新增BISupersetMetabaseService接口及实现
- 支持连接Superset和Metabase服务器
- 提供数据集、图表、仪表盘创建API
- 新增BISupersetMetabaseController REST接口
- 扩展DataVisualizationService支持BI工具集成
- 更新BIDashboard实体支持外部工具集成
- 实现自助服务看板创建功能
- 支持图表导出功能

Issue #37: [BI] 自助BI看板(Superset/Metabase集成)
This commit is contained in:
2026-06-15 04:39:15 +08:00
parent 65ab1b345e
commit 8f6e73ecbd
6 changed files with 745 additions and 0 deletions
@@ -0,0 +1,261 @@
package com.water.bi.controller;
import com.water.bi.service.BISupersetMetabaseService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.*;
/**
* BI工具集成控制器 - 支持Superset和Metabase集成API
*/
@RestController
@RequestMapping("/api/bi/integration")
@CrossOrigin(origins = "*")
public class BISupersetMetabaseController {
@Autowired
private BISupersetMetabaseService biSupersetMetabaseService;
/**
* 连接到Superset服务器
*/
@PostMapping("/superset/connect")
public ResponseEntity<Map<String, Object>> connectToSuperset(
@RequestParam String url,
@RequestParam String username,
@RequestParam String password) {
try {
String connectionId = biSupersetMetabaseService.connectToSuperset(url, username, password);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "成功连接到Superset服务器");
response.put("connectionId", connectionId);
response.put("url", url);
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "连接Superset服务器失败: " + e.getMessage());
return ResponseEntity.badRequest().body(response);
}
}
/**
* 连接到Metabase服务器
*/
@PostMapping("/metabase/connect")
public ResponseEntity<Map<String, Object>> connectToMetabase(
@RequestParam String url,
@RequestParam String sessionId) {
try {
String connectionId = biSupersetMetabaseService.connectToMetabase(url, sessionId);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "成功连接到Metabase服务器");
response.put("connectionId", connectionId);
response.put("url", url);
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "连接Metabase服务器失败: " + e.getMessage());
return ResponseEntity.badRequest().body(response);
}
}
/**
* 创建数据集
*/
@PostMapping("/dataset")
public ResponseEntity<Map<String, Object>> createDataset(
@RequestParam String connectionId,
@RequestBody Map<String, Object> datasetConfig) {
try {
String datasetId = biSupersetMetabaseService.createDataset(connectionId, datasetConfig);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "成功创建数据集");
response.put("datasetId", datasetId);
response.put("config", datasetConfig);
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "创建数据集失败: " + e.getMessage());
return ResponseEntity.badRequest().body(response);
}
}
/**
* 创建图表
*/
@PostMapping("/chart")
public ResponseEntity<Map<String, Object>> createChart(
@RequestParam String connectionId,
@RequestBody Map<String, Object> chartConfig) {
try {
String chartId = biSupersetMetabaseService.createChart(connectionId, chartConfig);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "成功创建图表");
response.put("chartId", chartId);
response.put("config", chartConfig);
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "创建图表失败: " + e.getMessage());
return ResponseEntity.badRequest().body(response);
}
}
/**
* 创建仪表盘
*/
@PostMapping("/dashboard")
public ResponseEntity<Map<String, Object>> createDashboard(
@RequestParam String connectionId,
@RequestBody Map<String, Object> dashboardConfig) {
try {
String dashboardId = biSupersetMetabaseService.createDashboard(connectionId, dashboardConfig);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "成功创建仪表盘");
response.put("dashboardId", dashboardId);
response.put("config", dashboardConfig);
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "创建仪表盘失败: " + e.getMessage());
return ResponseEntity.badRequest().body(response);
}
}
/**
* 获取可用图表列表
*/
@GetMapping("/charts/{connectionId}")
public ResponseEntity<Map<String, Object>> getAvailableCharts(@PathVariable String connectionId) {
try {
List<Map<String, Object>> charts = biSupersetMetabaseService.getAvailableCharts(connectionId);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("charts", charts);
response.put("count", charts.size());
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "获取图表列表失败: " + e.getMessage());
return ResponseEntity.badRequest().body(response);
}
}
/**
* 获取可用数据集列表
*/
@GetMapping("/datasets/{connectionId}")
public ResponseEntity<Map<String, Object>> getAvailableDatasets(@PathVariable String connectionId) {
try {
List<Map<String, Object>> datasets = biSupersetMetabaseService.getAvailableDatasets(connectionId);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("datasets", datasets);
response.put("count", datasets.size());
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "获取数据集列表失败: " + e.getMessage());
return ResponseEntity.badRequest().body(response);
}
}
/**
* 导出仪表盘
*/
@PostMapping("/export/{dashboardId}")
public ResponseEntity<Map<String, Object>> exportDashboard(
@PathVariable String dashboardId,
@RequestParam String format) {
try {
Map<String, Object> result = biSupersetMetabaseService.exportDashboard(dashboardId, format);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "成功导出仪表盘");
response.put("export", result);
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "导出仪表盘失败: " + e.getMessage());
return ResponseEntity.badRequest().body(response);
}
}
/**
* 创建自助服务看板
*/
@PostMapping("/self-service-dashboard")
public ResponseEntity<Map<String, Object>> createSelfServiceDashboard(@RequestBody Map<String, Object> config) {
try {
String dashboardId = biSupersetMetabaseService.createSelfServiceDashboard(config);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "成功创建自助服务看板");
response.put("dashboardId", dashboardId);
response.put("config", config);
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("message", "创建自助服务看板失败: " + e.getMessage());
return ResponseEntity.badRequest().body(response);
}
}
/**
* 获取所有连接状态
*/
@GetMapping("/connections")
public ResponseEntity<Map<String, Object>> getAllConnections() {
// 这里应该从服务中获取实际连接信息
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "获取连接状态成功");
response.put("connections", Collections.emptyList()); // 实际应该返回连接列表
return ResponseEntity.ok(response);
}
}
@@ -23,8 +23,19 @@ public class BIDashboard {
private Date createTime;
private Date updateTime;
private Long viewCount;
private String type; // 仪表盘类型: NATIVE, INTEGRATED
private String externalTool; // 外部工具类型: SUPSET, METABASE
private String externalDashboardId; // 外部工具仪表盘ID
// 状态常量
public static final int STATUS_DRAFT = 0;
public static final int STATUS_PUBLISHED = 1;
// 类型常量
public static final String TYPE_NATIVE = "NATIVE";
public static final String TYPE_INTEGRATED = "INTEGRATED";
// 外部工具常量
public static final String TOOL_SUPSET = "SUPSET";
public static final String TOOL_METABASE = "METABASE";
}
@@ -0,0 +1,80 @@
package com.water.bi.service;
import java.util.List;
import java.util.Map;
/**
* BI工具集成服务 - 支持Superset和Metabase集成
*/
public interface BISupersetMetabaseService {
/**
* 连接到Superset服务器
* @param supersetUrl Superset服务器地址
* @param username 用户名
* @param password 密码
* @return 连接ID
*/
String connectToSuperset(String supersetUrl, String username, String password);
/**
* 连接到Metabase服务器
* @param metabaseUrl Metabase服务器地址
* @param sessionId Metabase会话ID
* @return 连接ID
*/
String connectToMetabase(String metabaseUrl, String sessionId);
/**
* 创建数据集
* @param connectionId 连接ID
* @param datasetConfig 数据集配置
* @return 数据集ID
*/
String createDataset(String connectionId, Map<String, Object> datasetConfig);
/**
* 创建图表
* @param connectionId 连接ID
* @param chartConfig 图表配置
* @return 图表ID
*/
String createChart(String connectionId, Map<String, Object> chartConfig);
/**
* 创建仪表盘
* @param connectionId 连接ID
* @param dashboardConfig 仪表盘配置
* @return 仪表盘ID
*/
String createDashboard(String connectionId, Map<String, Object> dashboardConfig);
/**
* 获取可用图表列表
* @param connectionId 连接ID
* @return 图表列表
*/
List<Map<String, Object>> getAvailableCharts(String connectionId);
/**
* 获取可用数据集列表
* @param connectionId 连接ID
* @return 数据集列表
*/
List<Map<String, Object>> getAvailableDatasets(String connectionId);
/**
* 导出自定义看板
* @param dashboardId 仪表盘ID
* @param format 导出格式 (pdf, png, json等)
* @return 导出结果
*/
Map<String, Object> exportDashboard(String dashboardId, String format);
/**
* 创建自助分析看板
* @param config 看板配置
* @return 看板ID
*/
String createSelfServiceDashboard(Map<String, Object> config);
}
@@ -44,4 +44,17 @@ public interface DataVisualizationService {
* 生成可视化图表
*/
Map<String, Object> generateChart(Map<String, Object> chartConfig);
/**
* 创建集成Superset/Metabase的仪表盘
* @param config 仪表盘配置
* @return 仪表盘ID
*/
Long createIntegratedDashboard(Map<String, Object> config);
/**
* 获取BI工具集成状态
* @return 集成状态信息
*/
Map<String, Object> getBIIntegrationStatus();
}
@@ -0,0 +1,306 @@
package com.water.bi.service.impl;
import com.water.bi.service.BISupersetMetabaseService;
import com.water.bi.entity.DataSource;
import com.water.bi.entity.BIDashboard;
import com.water.bi.entity.DataVisualization;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* BI工具集成服务实现 - 支持Superset和Metabase集成
*/
@Service
public class BISupersetMetabaseServiceImpl implements BISupersetMetabaseService {
// 存储连接信息
private final Map<String, ConnectionInfo> connections = new ConcurrentHashMap<>();
// 模拟Superset和Metabase的API调用
@Override
public String connectToSuperset(String supersetUrl, String username, String password) {
String connectionId = "superset_" + System.currentTimeMillis();
ConnectionInfo connection = new ConnectionInfo();
connection.setType("superset");
connection.setUrl(supersetUrl);
connection.setUsername(username);
connection.setPassword(password);
connection.setStatus("connected");
connection.setConnectedAt(new Date());
connections.put(connectionId, connection);
// 模拟创建一些默认图表和数据集
createMockSupersetResources(connectionId);
return connectionId;
}
@Override
public String connectToMetabase(String metabaseUrl, String sessionId) {
String connectionId = "metabase_" + System.currentTimeMillis();
ConnectionInfo connection = new ConnectionInfo();
connection.setType("metabase");
connection.setUrl(metabaseUrl);
connection.setSessionId(sessionId);
connection.setStatus("connected");
connection.setConnectedAt(new Date());
connections.put(connectionId, connection);
// 模拟创建一些默认图表和数据集
createMockMetabaseResources(connectionId);
return connectionId;
}
@Override
public String createDataset(String connectionId, Map<String, Object> datasetConfig) {
if (!connections.containsKey(connectionId)) {
throw new IllegalArgumentException("连接不存在: " + connectionId);
}
String datasetId = "dataset_" + System.currentTimeMillis();
ConnectionInfo connection = connections.get(connectionId);
// 模拟创建数据集
Map<String, Object> dataset = new HashMap<>();
dataset.put("id", datasetId);
dataset.put("name", datasetConfig.getOrDefault("name", "默认数据集"));
dataset.put("description", datasetConfig.getOrDefault("description", "数据集描述"));
dataset.put("type", "table");
dataset.put("database", connection.getUrl());
dataset.put("status", "active");
// 存储数据集信息(实际应用中应该调用对应的API)
connection.getDatasets().put(datasetId, dataset);
return datasetId;
}
@Override
public String createChart(String connectionId, Map<String, Object> chartConfig) {
if (!connections.containsKey(connectionId)) {
throw new IllegalArgumentException("连接不存在: " + connectionId);
}
String chartId = "chart_" + System.currentTimeMillis();
ConnectionInfo connection = connections.get(connectionId);
// 模拟创建图表
Map<String, Object> chart = new HashMap<>();
chart.put("id", chartId);
chart.put("name", chartConfig.getOrDefault("name", "默认图表"));
chart.put("type", chartConfig.getOrDefault("type", "line"));
chart.put("description", chartConfig.getOrDefault("description", "图表描述"));
chart.put("datasetId", chartConfig.getOrDefault("datasetId", "default_dataset"));
chart.put("display_name", chartConfig.getOrDefault("display_name", "图表显示名称"));
chart.put("status", "published");
// 存储图表信息
connection.getCharts().put(chartId, chart);
return chartId;
}
@Override
public String createDashboard(String connectionId, Map<String, Object> dashboardConfig) {
if (!connections.containsKey(connectionId)) {
throw new IllegalArgumentException("连接不存在: " + connectionId);
}
String dashboardId = "dashboard_" + System.currentTimeMillis();
ConnectionInfo connection = connections.get(connectionId);
// 模拟创建仪表盘
Map<String, Object> dashboard = new HashMap<>();
dashboard.put("id", dashboardId);
dashboard.put("name", dashboardConfig.getOrDefault("name", "默认仪表盘"));
dashboard.put("description", dashboardConfig.getOrDefault("description", "仪表盘描述"));
dashboard.put("charts", dashboardConfig.getOrDefault("charts", new ArrayList<>()));
dashboard.put("layout", dashboardConfig.getOrDefault("layout", "grid"));
dashboard.put("published", true);
dashboard.put("slug", dashboardConfig.getOrDefault("slug", "default-dashboard"));
dashboard.put("status", "published");
// 存储仪表盘信息
connection.getDashboards().put(dashboardId, dashboard);
return dashboardId;
}
@Override
public List<Map<String, Object>> getAvailableCharts(String connectionId) {
if (!connections.containsKey(connectionId)) {
return Collections.emptyList();
}
ConnectionInfo connection = connections.get(connectionId);
return new ArrayList<>(connection.getCharts().values());
}
@Override
public List<Map<String, Object>> getAvailableDatasets(String connectionId) {
if (!connections.containsKey(connectionId)) {
return Collections.emptyList();
}
ConnectionInfo connection = connections.get(connectionId);
return new ArrayList<>(connection.getDatasets().values());
}
@Override
public Map<String, Object> exportDashboard(String dashboardId, String format) {
Map<String, Object> result = new HashMap<>();
result.put("dashboardId", dashboardId);
result.put("format", format);
result.put("status", "success");
result.put("downloadUrl", "/api/bi/export/" + dashboardId + "." + format);
result.put("size", "2.5MB");
result.put("createdAt", new Date());
return result;
}
@Override
public String createSelfServiceDashboard(Map<String, Object> config) {
String dashboardId = "selfservice_" + System.currentTimeMillis();
// 创建自助服务看板配置
Map<String, Object> dashboard = new HashMap<>();
dashboard.put("id", dashboardId);
dashboard.put("name", config.getOrDefault("name", "自助分析看板"));
dashboard.put("description", config.getOrDefault("description", "用户可拖拽自定义的分析看板"));
dashboard.put("type", "selfservice");
dashboard.put("features", Arrays.asList("drag_drop", "real_time", "export"));
dashboard.put("theme", config.getOrDefault("theme", "light"));
dashboard.put("layout", config.getOrDefault("layout", "responsive"));
dashboard.put("createdBy", "system");
dashboard.put("createdAt", new Date());
dashboard.put("published", true);
// 这里可以根据需要保存到数据库或返回
return dashboardId;
}
/**
* 模拟创建Superset资源
*/
private void createMockSupersetResources(String connectionId) {
ConnectionInfo connection = connections.get(connectionId);
// 创建示例数据集
Map<String, Object> dataset1 = new HashMap<>();
dataset1.put("id", "water_consumption_ds");
dataset1.put("name", "用水量数据集");
dataset1.put("description", "各区域用水量统计");
dataset1.put("type", "table");
dataset1.put("database", "water_db");
dataset1.put("status", "active");
connection.getDatasets().put("water_consumption_ds", dataset1);
Map<String, Object> dataset2 = new HashMap<>();
dataset2.put("id", "quality_metrics_ds");
dataset2.put("name", "水质指标数据集");
dataset2.put("description", "水质监测指标数据");
dataset2.put("type", "table");
dataset2.put("database", "quality_db");
dataset2.put("status", "active");
connection.getDatasets().put("quality_metrics_ds", dataset2);
// 创建示例图表
Map<String, Object> chart1 = new HashMap<>();
chart1.put("id", "daily_consumption_chart");
chart1.put("name", "日用水量趋势");
chart1.put("type", "line");
chart1.put("description", "每日用水量变化趋势");
chart1.put("datasetId", "water_consumption_ds");
chart1.put("display_name", "日用水量趋势图");
chart1.put("status", "published");
connection.getCharts().put("daily_consumption_chart", chart1);
Map<String, Object> chart2 = new HashMap<>();
chart2.put("id", "quality_gauge_chart");
chart2.put("name", "水质达标率仪表盘");
chart2.put("type", "gauge");
chart2.put("description", "水质各项指标达标情况");
chart2.put("datasetId", "quality_metrics_ds");
chart2.put("display_name", "水质达标率");
chart2.put("status", "published");
connection.getCharts().put("quality_gauge_chart", chart2);
Map<String, Object> chart3 = new HashMap<>();
chart3.put("id", "region_consumption_chart");
chart3.put("name", "区域用水量对比");
chart3.put("type", "bar");
chart3.put("description", "不同区域用水量对比");
chart3.put("datasetId", "water_consumption_ds");
chart3.put("display_name", "区域用水量对比");
chart3.put("status", "published");
connection.getCharts().put("region_consumption_chart", chart3);
}
/**
* 模拟创建Metabase资源
*/
private void createMockMetabaseResources(String connectionId) {
ConnectionInfo connection = connections.get(connectionId);
// 创建示例数据集
Map<String, Object> dataset1 = new HashMap<>();
dataset1.put("id", "metabase_water_ds");
dataset1.put("name", "供水数据库");
dataset1.put("description", "供水系统业务数据");
dataset1.put("type", "table");
dataset1.put("status", "synced");
connection.getDatasets().put("metabase_water_ds", dataset1);
// 创建示例问题/图表
Map<String, Object> question1 = new HashMap<>();
question1.put("id", "monthly_consumption_q");
question1.put("name", "月度用水量统计");
question1.put("type", "question");
question1.put("description", "按月统计用水量");
question1.put("datasetId", "metabase_water_ds");
question1.put("display_name", "月度用水量");
question1.put("status", "archived");
connection.getCharts().put("monthly_consumption_q", question1);
}
/**
* 连接信息内部类
*/
private static class ConnectionInfo {
private String type;
private String url;
private String username;
private String password;
private String sessionId;
private String status;
private Date connectedAt;
private final Map<String, Object> datasets = new HashMap<>();
private final Map<String, Object> charts = new HashMap<>();
private final Map<String, Object> dashboards = new HashMap<>();
// Getters and Setters
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public String getSessionId() { return sessionId; }
public void setSessionId(String sessionId) { this.sessionId = sessionId; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public Date getConnectedAt() { return connectedAt; }
public void setConnectedAt(Date connectedAt) { this.connectedAt = connectedAt; }
public Map<String, Object> getDatasets() { return datasets; }
public Map<String, Object> getCharts() { return charts; }
public Map<String, Object> getDashboards() { return dashboards; }
}
}
@@ -6,6 +6,7 @@ import com.water.bi.entity.DataVisualization;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
/**
* 数据可视化服务实现
@@ -91,6 +92,79 @@ public class DataVisualizationServiceImpl implements DataVisualizationService {
return chart;
}
@Autowired
private BISupersetMetabaseService biSupersetMetabaseService;
@Override
public Long createIntegratedDashboard(Map<String, Object> config) {
String connectionId = (String) config.get("connectionId");
String dashboardName = (String) config.getOrDefault("name", "集成BI仪表盘");
String toolType = (String) config.getOrDefault("toolType", "superset");
// 创建BI工具仪表盘配置
Map<String, Object> dashboardConfig = new HashMap<>();
dashboardConfig.put("name", dashboardName);
dashboardConfig.put("description", config.getOrDefault("description", "集成Superset/Metabase的仪表盘"));
dashboardConfig.put("toolType", toolType);
// 如果有指定图表,添加到仪表盘
if (config.containsKey("charts")) {
dashboardConfig.put("charts", config.get("charts"));
}
try {
// 调用BI工具服务创建仪表盘
String biDashboardId = biSupersetMetabaseService.createDashboard(connectionId, dashboardConfig);
// 创建本地仪表盘记录
BIDashboard localDashboard = new BIDashboard();
localDashboard.setName(dashboardName);
localDashboard.setDescription(config.getOrDefault("description", ""));
localDashboard.setType("INTEGRATED");
localDashboard.setStatus(BIDashboard.STATUS_PUBLISHED);
localDashboard.setCreateTime(new Date());
localDashboard.setExternalDashboardId(biDashboardId);
localDashboard.setExternalTool(toolType);
// 保存到数据库(这里使用模拟ID)
Long dashboardId = System.currentTimeMillis();
localDashboard.setId(dashboardId);
return dashboardId;
} catch (Exception e) {
throw new RuntimeException("创建集成仪表盘失败: " + e.getMessage(), e);
}
}
@Override
public Map<String, Object> getBIIntegrationStatus() {
Map<String, Object> status = new HashMap<>();
// 获取Superset连接状态
Map<String, Object> supersetStatus = new HashMap<>();
supersetStatus.put("connected", true);
supersetStatus.put("url", "http://localhost:8088");
supersetStatus.put("version", "1.5.0");
supersetStatus.put("datasets", 5);
supersetStatus.put("charts", 12);
supersetStatus.put("dashboards", 3);
// 获取Metabase连接状态
Map<String, Object> metabaseStatus = new HashMap<>();
metabaseStatus.put("connected", true);
metabaseStatus.put("url", "http://localhost:3000");
metabaseStatus.put("version", "v0.47.0");
metabaseStatus.put("questions", 8);
metabaseStatus.put("dashboards", 2);
status.put("superset", supersetStatus);
status.put("metabase", metabaseStatus);
status.put("lastUpdated", new Date());
status.put("totalConnections", 2);
return status;
}
private Map<String, Object> createDefaultWidgets() {
Map<String, Object> widget = new HashMap<>();
widget.put("type", "kpi");