# PisellSupersetDashboard

用于 SaaS 页面和低代码页面的 Superset 报表组件。宿主提供报表嵌入 UUID、UI 配置和已有请求上下文，组件负责获取会话、初始化 SDK、令牌续期、加载展示、失败重试及资源清理。

业务接口固定为 `POST /shop/report/superset/embed`。组件使用 `@superset-ui/embedded-sdk@0.4.0` 创建 iframe，不自行拼接带令牌的报表 URL。

## 1. 文件结构与职责

| 文件                                                 | 职责                                                 |
| ---------------------------------------------------- | ---------------------------------------------------- |
| `index.tsx`                                          | 组件入口、状态管理、SDK 初始化与清理、加载和错误交互 |
| `types.ts`                                           | 公开 Props，以及内部请求客户端和会话类型             |
| `serve.ts`                                           | 业务接口调用、响应校验和报表异常类型                 |
| `locales.ts`                                         | 英文、简体中文、繁体中文文案                         |
| `index.less`                                         | 容器尺寸、iframe、加载图标和错误遮罩样式             |
| `index.test.tsx`                                     | 组件行为、续期、超时和异步竞态测试                   |
| `serve.test.ts`                                      | 请求契约、业务错误和响应字段校验测试                 |
| `../../../lowcode/pisell-superset-dashboard/meta.ts` | 低代码物料描述、属性编辑器及默认片段                 |

包入口 `packages/private-materials/src/index.ts` 导出 `PisellSupersetDashboard` 和 `PisellSupersetDashboardProps`。组件目录同时提供默认导出与具名导出。

## 2. 基础使用

以下组件应渲染在宿主已有的引擎上下文 Provider 内：

```tsx
import {
  PisellSupersetDashboard,
  type PisellSupersetDashboardProps,
} from '@pisell/private-materials';

/** 宿主页面保存的报表配置；示例 UUID 需在目标环境核实。 */
const reportConfig: PisellSupersetDashboardProps['proCodeProps'] = {
  dashboardId: '6c442e22-dccf-48bd-b159-7569d497ab56',
  dashboardUiConfig: {
    filters: { visible: true, expanded: false },
    hideTitle: true,
    hideChartControls: true,
  },
};

/** 已位于宿主引擎上下文内的报表页面。 */
export function ReportPage() {
  return (
    <PisellSupersetDashboard
      proCodeProps={reportConfig}
      style={{ height: 'calc(100vh - 120px)', minHeight: 320 }}
    />
  );
}
```

组件接收的完整页面属性结构：

```json
{
  "proCodeProps": {
    "dashboardId": "6c442e22-dccf-48bd-b159-7569d497ab56",
    "dashboardUiConfig": {
      "filters": { "visible": true, "expanded": false },
      "hideTitle": true,
      "hideChartControls": true
    }
  }
}
```

`dashboardId` 来自 Superset Dashboard 的嵌入配置，必须使用嵌入 UUID。普通数字 Dashboard ID 和完整报表 URL 均不适用。

## 3. Props 与类型

```ts
interface PisellSupersetDashboardProps {
  /** 宿主页面下发的报表配置。 */
  proCodeProps?: {
    /** 报表嵌入 UUID。 */
    dashboardId: string;
    /** 完整透传给 SDK 的 UI 配置。 */
    dashboardUiConfig?: Record<string, any>;
  };
  /** 根容器附加样式类名。 */
  className?: string;
  /** 根容器样式，可覆盖默认宽高。 */
  style?: React.CSSProperties;
}
```

| 属性                             | 类型                        | 行为                                         |
| -------------------------------- | --------------------------- | -------------------------------------------- |
| `proCodeProps`                   | 对象，可选                  | 缺失时显示配置提示，不发请求                 |
| `proCodeProps.dashboardId`       | `string`                    | 非空时请求后端；UUID 格式由后端最终校验      |
| `proCodeProps.dashboardUiConfig` | `Record<string, any>`，可选 | 原样传给 SDK；未提供时沿用 SDK 默认行为      |
| `className`                      | `string`，可选              | 追加到组件根容器                             |
| `style`                          | `React.CSSProperties`，可选 | 覆盖默认宽 `100%`、高 `75vh`、最小高 `520px` |

组件不合并默认 UI 配置、不筛除扩展字段，也不把 UI 配置提交给业务接口。具体字段是否生效取决于当前 SDK 和 Superset 服务端的支持情况。

父组件传入内容相同的新配置对象不会重建报表。嵌套字段变化会触发重新初始化；使用深拷贝快照，也能识别原对象被修改后发生重新渲染的情况。仅修改对象而没有触发 React 渲染，不会自动触发更新。

当前没有额外的事件 Props、命令式方法、`request`、`token` 或 Superset 域名 Props。点击内置重试按钮即可重新初始化。

## 4. 宿主上下文与请求方式

组件从以下位置获取已有服务：

```ts
/** 宿主引擎上下文。 */
const context = useEngineContext();
/** 宿主请求客户端，需至少提供 post 方法。 */
const client = context?.appHelper?.utils?.request;
```

宿主需要通过现有引擎 Context Provider 提供 `appHelper.utils.request`。普通 proCode 页面也需要该上下文，单独渲染组件不会自动获得业务请求服务。上下文缺失时组件显示提示，不尝试从共享单例或浏览器存储寻找备用客户端。

请求客户端负责：

- 选择当前环境的业务 API baseURL，并支持 `isShopApi: true`。
- 注入已选店铺的有效 Bearer Token，处理登录 Token 更新和登录失效。
- 支持第三个参数中的 `AbortSignal`，以及 `fullResult: true`。
- 返回已解包的业务响应主体，而非原始 `AxiosResponse`。

组件直接将当前客户端传给 `getSupersetSession`，不调用 `request.setRequest()`。这样不会覆盖组件包的共享客户端配置。客户端引用独立不代表内部登录态被固定，切店顺序仍须由宿主保证。

直接使用客户端也不会自动注入共享包装层的 `customToast`；本组件通过错误遮罩展示异常，宿主自己的拦截器和错误处理仍会执行。

## 5. 接口契约与 SDK 映射

每次会话请求均为：

```ts
client.post(
  '/shop/report/superset/embed',
  { dashboard_id: dashboardId },
  { isShopApi: true, fullResult: true, signal }
);
```

请求体只提交 `dashboard_id`。店铺身份来自宿主登录态，`shop_id`、RLS、服务账号凭据、UI 配置和 Superset 域名均不由组件提交。

成功业务响应示例：

```json
{
  "status": true,
  "code": 200,
  "message": "",
  "data": {
    "guest_token": "当前店铺的短期访客令牌",
    "superset_domain": "https://bi.mypisell.com",
    "dashboard_id": "6c442e22-dccf-48bd-b159-7569d497ab56"
  }
}
```

| 数据来源                         | SDK 参数或用途                                      |
| -------------------------------- | --------------------------------------------------- |
| `data.dashboard_id`              | `embedDashboard.id`，采用实际响应值                 |
| `data.superset_domain`           | `embedDashboard.supersetDomain`                     |
| `data.guest_token`               | `fetchGuestToken` 返回的字符串                      |
| `proCodeProps.dashboardUiConfig` | `embedDashboard.dashboardUiConfig`                  |
| 本轮独立 DOM 节点                | `embedDashboard.mountPoint`                         |
| 固定来源策略                     | `referrerPolicy: 'strict-origin-when-cross-origin'` |

接口适配层同时要求 `status === true`、`code === 200`，并检查三个会话字段都是非空字符串。HTTP 200 不能单独作为成功依据。业务失败时优先展示 `message`；没有可用消息时使用国际化兜底文案。

`SupersetReportError` 是内部异常类型，包含只读 `textKey` 和继承的 `message`。宿主拒绝的请求不会在接口层被改写，组件在上层统一处理其展示。

## 6. 初始化与令牌续期

```mermaid
flowchart TD
  A[组件挂载或配置变化] --> B{报表 ID 和客户端可用?}
  B -- 否 --> C[显示配置错误]
  B -- 是 --> D[显示加载状态并申请会话]
  D --> E[使用响应域名和 ID 初始化 SDK]
  E --> F[首次取令牌复用本轮已有令牌]
  F --> G[SDK 初始化完成，隐藏加载状态]
  G --> H[SDK 后续调用取令牌回调]
  H --> I[重新请求业务接口]
  I --> G
  D -- 失败或初始化超时 --> J[清理当前会话并显示错误]
  E -- 初始化失败 --> J
  I -- 续期失败 --> J
  J --> K[用户点击重新加载]
  K --> A
```

每轮初始化维护以下内部状态：

| 标识                              | 含义                                       |
| --------------------------------- | ------------------------------------------ |
| `INITIALIZATION_TIMEOUT`          | 当前为 `60000` 毫秒，即 60 秒初始化超时    |
| `configSnapshot` / `stableConfig` | 比较 UI 配置内容，避免等值对象触发重复挂载 |
| `host` / `mountPoint`             | React 容器和本轮 SDK 专属节点              |
| `attempt`                         | 手动重试序号                               |
| `loading` / `error`               | 加载状态及可展示错误                       |
| `controller`                      | 本轮业务请求的取消控制器                   |
| `stopped`                         | 本轮会话是否已失效                         |
| `instance`                        | 已完成初始化的 SDK 实例                    |
| `firstToken`                      | 仅允许首次回调消费一次的令牌               |
| `timer`                           | 首次初始化的超时计时器                     |

首次业务请求成功后，`firstToken` 供 SDK 第一次调用时返回，随后立即清空。后续每次 SDK 续期回调都重新调用同一业务接口，返回新的 `guest_token` 字符串。组件不自行高频轮询，也不写死 Guest Token 的有效期。

60 秒计时覆盖首次业务请求和 SDK 初始化，初始化完成即清除；它不限制图表数据加载时间，也不是令牌有效期或后续所有请求的统一超时时间。请求本身的超时仍由宿主客户端处理，SDK 的内部取令牌超时遵循所用版本的实现。

## 7. 清理、竞态与切店

| 内部方法             | 职责                                             |
| -------------------- | ------------------------------------------------ |
| `getSupersetSession` | 调用业务接口并校验响应                           |
| `getSession`         | 在请求前后检查会话有效性，拒绝迟到响应           |
| `assertActive`       | 阻止失效会话继续请求或返回令牌                   |
| `dispose`            | 标记失效、取消请求、清空令牌、卸载实例并移除节点 |
| `fail`               | 清理失败会话，并在当前有效页面显示错误           |

组件卸载、报表 ID 变化、UI 配置内容变化、客户端引用变化或手动重试时，都会先结束旧会话。

每轮使用独立的 DOM 节点和取消控制器。即便请求客户端未真正中止网络请求，响应返回后也会检查 `stopped`。SDK 创建期间如果发生卸载或超时，迟到的 SDK 实例会立即卸载，不会保存到新会话，也不会清空新报表容器。

切店或退出登录必须由宿主按以下顺序执行：

1. 先卸载当前报表组件，结束旧实例及续期回调。
2. 完成店铺登录态切换或退出登录。
3. 新登录上下文准备好后，再渲染目标报表。

宿主可使用 `key={currentShopId}` 区分报表实例，但在登录态更新后才更换 `key` 不能替代上述卸载顺序。不要通过路由保活、隐藏 DOM 或预渲染持续保留旧报表。组件不会主动监听外部存储中的店铺或 Token 变化。

Guest Token 只用于当前会话，不写入 URL、日志或浏览器持久存储，不跨店铺或报表复用。

## 8. 加载、失败与样式

加载状态由 Ant Design `Spin` 和 `LoadingOutlined` 组成，图标为 28px，置于 56px 的浅灰圆角底框内，下方展示加载文案。图标颜色沿用 Ant Design 主题。系统设置减少动画时关闭旋转，保留图标和文案。

根容器通过 `aria-busy` 表示初始化状态，加载区使用 `role="status"`，错误区使用 `role="alert"`；装饰图标使用 `aria-hidden` 避免重复播报。

默认容器宽度为 `100%`，高度为 `75vh`，最小高度为 `520px`。使用更小高度时，应同时覆盖 `minHeight`。iframe 填满容器，无边框。

| 场景                      | 组件行为                         |
| ------------------------- | -------------------------------- |
| 缺少报表 ID 或请求客户端  | 不发请求，显示配置提示           |
| HTTP 成功但业务失败       | 显示业务消息或兜底文案           |
| 成功响应缺少会话字段      | 显示报表配置不完整               |
| 首次请求或 SDK 初始化失败 | 清理本轮资源，显示重新加载按钮   |
| 初始化超过 60 秒          | 标记超时，取消请求，清理挂载节点 |
| 续期请求失败              | 停止当前实例，等待手动重新加载   |
| 主动卸载或被新会话替代    | 静默清理，不展示旧会话错误       |

加载提示消失只表示 SDK 初始化完成，不表示每张图表的数据都加载成功。跨域 iframe 中的图表错误不在组件的直接检测范围内。

`locales.ts` 提供 `en`、`zh-CN`、`zh-HK` 文案，显示语言来自 `context.engine.props.locale`，未提供时使用 `en`。后端返回的错误消息按原文展示。

## 9. 低代码与构建

低代码物料名称和包导出名称均为 `PisellSupersetDashboard`。属性面板使用 `ObjectSetter` 编辑 `proCodeProps`，其中 `dashboardId` 使用字符串编辑器，`dashboardUiConfig` 使用 JSON 编辑器。

默认物料片段将报表 ID 留空，预置筛选器可见且收起、隐藏标题和图表操作项；这些设置只属于片段默认值，组件运行时不会强制添加。

SDK 固定版本为 `0.4.0`，其发布文件包含 Webpack 4 不支持的现代语法。组件包 `build.plugin.js` 已将该 SDK 纳入已有 Babel 转译规则。开发环境建议使用 Node.js 18；现有低代码构建插件在 Node.js 22 下存在类型文件加载兼容问题。

在仓库根目录执行：

```bash
# 运行组件与接口测试。
pnpm --filter @pisell/private-materials exec vitest run src/plus/pisellSupersetDashboard --maxWorkers=1 --minWorkers=1

# 生成组件包 ESM/CJS 产物和类型声明。
pnpm --filter @pisell/private-materials build

# 使用 Node.js 18 执行现有 Webpack 4 低代码构建。
NODE_OPTIONS=--openssl-legacy-provider pnpm --filter @pisell/private-materials lowcode:build
```

自动测试覆盖配置透传、响应映射、首次令牌复用、续期失败、手动重试、配置变化、初始化超时、卸载后的迟到响应及多实例隔离。测试使用模拟客户端和 SDK，不会签发真实令牌。

## 10. 联调与常见问题

| 现象                     | 核对事项                                                                |
| ------------------------ | ----------------------------------------------------------------------- |
| 提示宿主未提供请求客户端 | 确认当前页面处于引擎 Provider 内，并提供 `appHelper.utils.request.post` |
| 提示缺少报表嵌入 ID      | 检查配置是否放在 `proCodeProps.dashboardId`                             |
| 后端提示 UUID 不合法     | 确认使用嵌入配置生成的 UUID                                             |
| 接口成功但提示配置不完整 | 检查客户端解包层级，以及三个会话字段是否齐全                            |
| iframe 空白或拒绝嵌入    | 检查返回域名、嵌入允许来源、Referer、CSP 和容器尺寸                     |
| 图表显示但无数据         | 检查 Superset 数据集权限、RLS 和目标店铺数据                            |
| 切店后仍看到旧内容       | 检查是否先卸载旧组件，以及路由是否保活                                  |
| 重复挂载或重复申请令牌   | 检查报表配置、客户端引用和 React key 是否持续变化                       |

Superset 需要启用嵌入能力并允许实际前端来源；业务 API 跨域访问还需正确配置 CORS。服务账号、RLS、数据集权限和店铺隔离由后端及 Superset 配置负责。

正式接入后仍需验证真实报表展示、长时间打开时的续期、离开页面后的清理，以及 A/B 店铺的数据隔离。组件包导出和物料注册不等于宿主已接通 `superset_dashboard` 页面，该页面映射需要由宿主完成。
