# 用纯 Python 构建可交互 Dashboard：Prefab 响应式 UI 与静态 HTML 导出教程

- 原文标题：How to Design Python-First Interactive Dashboards with Prefab Reactive UI Components and Static HTML Export
- 原文链接：https://www.marktechpost.com/2026/06/21/how-to-design-python-first-interactive-dashboards-with-prefab-reactive-ui-components-and-static-html-export/
- Prefab 文档：https://prefab.prefect.io/docs/welcome
- 原文环境：Google Colab，`prefab-ui==0.20.2`
- 本文定位：基于原文整理的中文分享教程；代码分为“可本地运行的数据逻辑”和“需要安装 Prefab 的 UI 片段”。

## 适合谁读

如果你会 Python，但不想为了一个内部运营面板临时写 React、打包前端、配置 Node.js 工具链，Prefab 这类 Python-first UI 框架就值得试一下。

它适合：

- 数据分析师快速把脚本结果做成可点击面板；
- 算法或平台工程师做内部演示 dashboard；
- 团队把静态 HTML 嵌入文档、Wiki 或邮件附件；
- 不需要登录、多用户协作和数据库写入的轻量交互页面。

不适合直接拿来做：

- 有权限控制的生产后台；
- 需要多人同时写入的业务系统；
- 需要后端数据库持久化的工单、审批、运营系统。

核心边界一句话：**Prefab 静态 HTML 导出能做客户端交互，但刷新页面后客户端状态会丢失。**

## 你将完成什么

本文把原文的运维仪表盘拆成 5 步：

1. 准备 Colab 或本地 Python 环境。
2. 生成模拟 pipeline 监控数据。
3. 计算 KPI、区域聚合和高优先级 watchlist。
4. 用 Prefab 组件声明 Dashboard 页面。
5. 导出为静态 HTML，并在 Colab 中预览。

最终 Dashboard 包含：

- `Overview`：KPI、趋势图、SLO 进度、状态分布；
- `Run Explorer`：可搜索、排序、分页的数据表；
- `Diagnostics`：区域散点图、雷达图、高优先级列表；
- `Triage Notes`：客户端本地 notes 表单；
- `Architecture`：解释数据、组件树、React 渲染和客户端状态的关系。

## 第 1 步：准备环境

原文在 Google Colab 中运行，固定 Prefab 版本：

```python
import base64
import subprocess
import sys
from pathlib import Path

from IPython.display import FileLink, HTML, display

PREFAB_VERSION = "0.20.2"
APP_PATH = Path("/content/prefab_advanced_tutorial_app.py")
HTML_PATH = Path("/content/prefab_advanced_dashboard.html")

subprocess.check_call([
    sys.executable,
    "-m",
    "pip",
    "install",
    "-q",
    f"prefab-ui=={PREFAB_VERSION}",
])
```

如果你在本地环境试验，也可以先建一个干净虚拟环境，再安装：

```bash
python3 -m venv .venv
source .venv/bin/activate
python -m pip install 'prefab-ui==0.20.2'
```

> 注意：本文只是分享教程，不建议在已有生产项目里直接新增依赖。先用隔离环境验证，再决定是否纳入项目依赖。

## 第 2 步：先把数据逻辑跑通

不要一上来就写 UI。先确认你的数据结构能支撑 Dashboard。

下面是一个可直接本地运行的精简版数据生成和聚合脚本，使用 Python 标准库，不依赖 Prefab。

```python
from __future__ import annotations

import random
from collections import Counter, defaultdict
from datetime import date, timedelta
from typing import Any

REGIONS = ["APAC", "EMEA", "NA", "LATAM"]
PIPELINES = ["LLM Triage", "Risk Scoring", "Warehouse Load"]
STATES = ["Completed", "Completed", "Completed", "Late", "Failed"]
PRIORITIES = ["P0", "P1", "P2", "P3"]


def build_runs(days: int = 10) -> list[dict[str, Any]]:
    random.seed(42)
    today = date.today()
    runs: list[dict[str, Any]] = []

    for day_offset in range(days):
        run_date = today - timedelta(days=days - day_offset - 1)
        for region in REGIONS:
            for index in range(random.randint(8, 16)):
                state = random.choice(STATES)
                duration = random.randint(40, 220)
                runs.append({
                    "run_id": f"{run_date:%m%d}-{region[:2]}-{index:03d}",
                    "date": run_date.isoformat(),
                    "region": region,
                    "pipeline": random.choice(PIPELINES),
                    "state": state,
                    "priority": random.choice(PRIORITIES),
                    "duration_s": duration,
                    "sla_gap": round(max(0, duration - 120) / 60, 1),
                })

    return runs


def summarize_by_region(runs: list[dict[str, Any]]) -> list[dict[str, Any]]:
    buckets: dict[str, list[dict[str, Any]]] = defaultdict(list)
    for run in runs:
        buckets[run["region"]].append(run)

    rows: list[dict[str, Any]] = []
    for region, items in buckets.items():
        failures = sum(1 for item in items if item["state"] == "Failed")
        late = sum(1 for item in items if item["state"] == "Late")
        success_rate = round(100 * (len(items) - failures - late * 0.35) / len(items), 1)
        rows.append({
            "region": region,
            "runs": len(items),
            "failures": failures,
            "late": late,
            "success_rate": success_rate,
            "avg_latency": round(sum(item["duration_s"] for item in items) / len(items), 1),
        })
    return sorted(rows, key=lambda row: row["region"])


def make_watchlist(runs: list[dict[str, Any]]) -> list[dict[str, Any]]:
    risky = [run for run in runs if run["state"] in {"Failed", "Late"}]
    return sorted(risky, key=lambda run: (run["priority"], -run["sla_gap"]))[:8]


if __name__ == "__main__":
    runs = build_runs()
    status_counts = Counter(run["state"] for run in runs)
    print("status_counts", dict(status_counts))
    print("regions", summarize_by_region(runs))
    print("watchlist_size", len(make_watchlist(runs)))
```

预期输出形态：

```text
status_counts {'Completed': ..., 'Late': ..., 'Failed': ...}
regions [{'region': 'APAC', 'runs': ..., 'success_rate': ...}, ...]
watchlist_size 8
```

这一步的目标不是生成漂亮图表，而是先确认三件事：

- Dashboard 的行数据字段是否稳定；
- KPI 是否能从行数据确定性计算出来；
- watchlist、状态分布、区域聚合是否能独立于 UI 测试。

## 第 3 步：把数据塞进 Prefab 的初始状态

Prefab 的核心思路是：**Python 负责准备数据和声明组件树，前端运行时负责渲染和客户端交互。**

原文使用 `initial_state` 保存当前页面需要的状态，例如：

```python
initial_state = {
    "selected_region": "All",
    "line_rows": DAILY_BY_REGION["All"],
    "table_rows": RUNS_BY_REGION["All"],
    "status_rows": STATUS_BY_REGION["All"],
    "pipeline_rows": PIPELINE_BY_REGION["All"],
    "region_kpis": KPI_BY_REGION["All"],
    "selected_run": None,
    "slo_target": 94,
    "operator_name": "Colab Builder",
    "notes": [{"note": "Click a run row to inspect it, then add triage notes here."}],
    "watchlist": WATCHLIST,
    "compact": False,
    "dark_mode": False,
}
```

这里有一个很重要的设计习惯：

- 图表不要直接读取全量原始数据；
- 先在 Python 侧准备好 `line_rows`、`table_rows`、`status_rows` 等结构；
- UI 只绑定这些面向展示的状态。

这样后面做区域过滤、表格点击、notes 添加时，状态变化的边界更清楚。

## 第 4 步：用 Prefab 组件声明页面

Prefab 的写法接近“用 Python 声明 React 组件树”。原文用 `with` 块组织布局：

```python
from prefab_ui.app import PrefabApp
from prefab_ui.components import Badge, Button, Card, CardContent, CardHeader, CardTitle, Column, Grid, H2, Muted, Row, Slider, Tabs, Tab
from prefab_ui.rx import STATE

with PrefabApp(
    title="Prefab Advanced Colab Tutorial",
    state=initial_state,
    css_class="max-w-7xl mx-auto p-6",
) as app:
    with Column(gap=6):
        with Row(justify="between", align="center", gap=4):
            with Column(gap=1):
                H2("Prefab Advanced Operations Dashboard")
                Muted("Everything below is composed in Python.")
            Badge(f"Region: {STATE.selected_region}", variant="info")

        with Tabs(value="overview"):
            with Tab("Overview", value="overview"):
                with Card():
                    with CardHeader():
                        CardTitle("Interactive controls")
                    with CardContent():
                        Slider(
                            value=STATE.slo_target,
                            min=80,
                            max=99,
                            step=0.5,
                        )
```

这段代码说明了 Prefab 的几个基本概念：

- `PrefabApp` 是应用入口；
- `Column`、`Row`、`Grid` 负责布局；
- `Card`、`Badge`、`Metric`、`DataTable`、`Chart` 等是预制 UI 组件；
- `STATE.xxx` 会绑定到客户端状态；
- `Tabs` / `Tab` 用于把复杂页面拆成多个区域。

## 第 5 步：让控件真正改变状态

只有静态组件还不够。Dashboard 的价值在于“点一下，页面就变”。

原文的区域过滤按钮会一次性更新多组状态：

```python
from prefab_ui.actions import SetState, ShowToast

REGION_ACTIONS = {
    region: [
        SetState("selected_region", region),
        SetState("line_rows", DAILY_BY_REGION[region]),
        SetState("table_rows", RUNS_BY_REGION[region]),
        SetState("status_rows", STATUS_BY_REGION[region]),
        SetState("pipeline_rows", PIPELINE_BY_REGION[region]),
        SetState("region_kpis", KPI_BY_REGION[region]),
        SetState("selected_run", None),
        ShowToast(f"Region set to {region}", variant="info", duration=1800),
    ]
    for region in REGIONS
}
```

这类 action 列表适合处理“点击一个按钮后，同时刷新多个组件”的场景。

常见状态动作可以这样理解：

| 动作 | 用途 | 典型场景 |
|---|---|---|
| `SetState` | 替换一个状态值 | 切换区域、选中表格行、更新 SLO 目标 |
| `AppendState` | 向列表追加一项 | 添加 triage note |
| `PopState` | 从列表移除一项 | 删除最新 note |
| `ToggleState` | 布尔值取反 | 切换 compact / dark flag |
| `ShowToast` | 展示提示 | 点击过滤、保存表单后的反馈 |

## 示例 1：Overview 页面怎么设计

`Overview` 页负责给读者快速回答三个问题：

1. 当前系统整体是否健康？
2. 哪个区域、pipeline 或状态最值得关注？
3. SLO 是否达标？

原文组合了这些组件：

- `Metric`：展示 Runs、Success rate、Avg latency、ROI index；
- `LineChart`：展示成功率和平均延迟趋势；
- `Progress` / `Ring`：展示 SLO 达标程度；
- `If` / `Else`：根据成功率是否超过目标展示不同 badge；
- `PieChart`：展示 open issue 状态分布；
- `BarChart`：展示受影响 pipeline 分布。

实践建议：Overview 不要堆太多表格。它应该先给管理者或值班同学一个“是否异常”的判断入口。

## 示例 2：Run Explorer 页面怎么设计

`Run Explorer` 页负责下钻。

原文用 `DataTable` 展示运行记录，并打开：

- 搜索；
- 排序；
- 分页；
- 行点击事件。

核心交互是：点击一行，把整行对象写入 `selected_run`，然后条件渲染详情面板。

```python
from prefab_ui.actions import SetState
from prefab_ui.components import DataTable, DataTableColumn
from prefab_ui.components.control_flow import If
from prefab_ui.rx import EVENT, STATE

DataTable(
    columns=[
        DataTableColumn(key="run_id", header="Run ID", sortable=True),
        DataTableColumn(key="date", header="Date", sortable=True),
        DataTableColumn(key="pipeline", header="Pipeline", sortable=True),
        DataTableColumn(key="region", header="Region", sortable=True),
        DataTableColumn(key="state", header="State", sortable=True),
        DataTableColumn(key="priority", header="Priority", sortable=True),
    ],
    rows=STATE.table_rows,
    search=True,
    paginated=True,
    pageSize=12,
    onRowClick=SetState("selected_run", EVENT),
)

with If(STATE.selected_run):
    # 这里渲染 selected_run 的详情卡片
    pass
```

这个模式可以复用到很多内部工具：订单列表、告警列表、模型评估样本、任务队列、异常日志。

## 示例 3：Triage Notes 页面怎么设计

原文的 notes 表单是一个很好的边界示例：它能展示 Prefab 的客户端交互能力，但也暴露了静态 HTML 的限制。

```python
from prefab_ui.actions import AppendState, PopState, ShowToast
from prefab_ui.components import Button, Form, Input
from prefab_ui.components.control_flow import ForEach

with Form(
    on_submit=[
        AppendState("notes", EVENT),
        ShowToast("Note added", variant="success"),
    ]
):
    Input(
        name="note",
        placeholder="Example: Escalate APAC LLM Triage failures",
        required=True,
    )
    Button("Add note", buttonType="submit")

Button(
    "Remove latest note",
    onClick=PopState("notes", -1),
)

with ForEach("notes") as note:
    # 这里渲染 note.note
    pass
```

关键提醒：这些 notes 只存在于浏览器当前页面状态中。刷新页面后会丢失。  
如果你的业务真的需要保存 notes，就必须接入后端 API 或数据库，不能只靠静态 HTML。

## 第 6 步：导出静态 HTML

原文使用 Prefab CLI 导出：

```bash
prefab export /content/prefab_advanced_tutorial_app.py -o /content/prefab_advanced_dashboard.html
```

本地开发时可以用：

```bash
prefab serve /content/prefab_advanced_tutorial_app.py --reload
```

在 Colab 中，原文把导出的 HTML 读成 bytes，再 base64 编码嵌入 iframe：

```python
html_bytes = HTML_PATH.read_bytes()
encoded = base64.b64encode(html_bytes).decode("utf-8")

display(HTML(f"""
<iframe
  src="data:text/html;base64,{encoded}"
  width="100%"
  height="950"
  style="border: 1px solid #ddd; border-radius: 12px;"
></iframe>
"""))
```

这样不用部署服务器，也能在 notebook 里直接预览完整交互页面。

## 验收清单

做完后不要只看“页面能打开”。建议按下面顺序检查：

- 数据层：
  - `runs` 是否有稳定字段：`run_id`、`date`、`pipeline`、`region`、`state`、`priority`、`duration_s`；
  - KPI 是否能从数据确定性计算；
  - watchlist 是否只包含高风险项。
- UI 层：
  - 区域按钮是否能切换图表和表格；
  - SLO slider 是否能改变目标展示；
  - DataTable 是否能搜索、排序、分页；
  - 点击表格行后是否出现详情面板；
  - notes 表单是否能追加和删除本地状态。
- 导出层：
  - `prefab export` 是否生成非空 HTML；
  - HTML 是否能在无后端服务的情况下打开；
  - 刷新页面后状态丢失是否符合预期。

## 常见坑

### 1. 把静态 Dashboard 当成生产后台

静态 HTML 适合演示、汇报和轻量交互，不等于生产后台。  
一旦需要权限、审计、多人协作、持久化，就要补后端服务。

### 2. 直接把全量明细塞进前端

原文示例把表格限制到最长 80 条，并按优先级排序。这个设计是合理的：静态页面不适合承载无限大数据集。

### 3. UI 状态和业务数据混在一起

建议分清：

- 原始或聚合数据：`line_rows`、`table_rows`、`status_rows`；
- 页面控制状态：`selected_region`、`selected_run`、`slo_target`；
- 临时交互状态：`notes`、`compact`、`dark_mode`。

### 4. 只做图表，不做验证

Dashboard 的风险不是图表不漂亮，而是数据口径错误。先跑通数据聚合脚本，再接 UI。

## 推荐落地路径

如果你要在团队里试用 Prefab，不建议一次做完整生产面板。可以按三阶段推进：

1. **第一阶段：静态演示**  
   用脱敏 CSV 或模拟数据生成一个只读 Dashboard，验证组件、交互和静态导出。

2. **第二阶段：内部报告**  
   把每日或每周批处理结果导出成 HTML，作为邮件附件或 Wiki 附件分发。

3. **第三阶段：生产替代评估**  
   如果需要真实业务操作，再评估后端 API、鉴权、日志、权限、持久化和部署方式。不要默认静态导出能覆盖这些需求。

## 一句话总结

Prefab 的价值不是“让 Python 变成前端框架”，而是给 Python 用户一条快速路径：先把数据逻辑留在 Python，再用预制组件拼出可交互页面，最后导出为便于分享的静态 HTML。它很适合原型、演示和内部报告；但只要涉及持久化和权限，就必须回到后端架构设计。
