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

适合谁读

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

它适合:

不适合直接拿来做:

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

你将完成什么

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

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

最终 Dashboard 包含:

第 1 步:准备环境

原文在 Google Colab 中运行,固定 Prefab 版本:

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}",
])

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

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

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

第 2 步:先把数据逻辑跑通

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

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

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)))

预期输出形态:

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

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

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

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

原文使用 initial_state 保存当前页面需要的状态,例如:

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,
}

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

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

第 4 步:用 Prefab 组件声明页面

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

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 的几个基本概念:

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

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

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

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 是否达标?

原文组合了这些组件:

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

示例 2:Run Explorer 页面怎么设计

Run Explorer 页负责下钻。

原文用 DataTable 展示运行记录,并打开:

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

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 的限制。

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 导出:

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

本地开发时可以用:

prefab serve /content/prefab_advanced_tutorial_app.py --reload

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

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 里直接预览完整交互页面。

验收清单

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

常见坑

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

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

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

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

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

建议分清:

4. 只做图表,不做验证

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

推荐落地路径

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

  1. 第一阶段:静态演示

用脱敏 CSV 或模拟数据生成一个只读 Dashboard,验证组件、交互和静态导出。

  1. 第二阶段:内部报告

把每日或每周批处理结果导出成 HTML,作为邮件附件或 Wiki 附件分发。

  1. 第三阶段:生产替代评估

如果需要真实业务操作,再评估后端 API、鉴权、日志、权限、持久化和部署方式。不要默认静态导出能覆盖这些需求。

一句话总结

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