PostHog 仪表盘 Widget 布局与 UX 技术指南:Catalog 栅格体系、编辑交互与实战配置
PostHog 的 Dashboard Widget(仪表盘小组件)体系允许把 Error Tracking、Session Replay、Experiments、Surveys、Logs 等不同产品的上下文汇聚到同一个看板。本文以仓库内 .agents/skills/manage-dashboard-widgets/references/layout-and-ux.md 为骨架,结合 tileLayouts.ts、catalog.ts、widget_layouts.py 等源码实现,完整讲解:栅格尺寸(min/max)的单一事实来源与传递链路、Add Widget 弹窗的多选与底部堆叠放置逻辑、NEW 徽标、⋯ 菜单、头布局选择、日期范围配置与描述展示等全部 UX 细节。读完你可以精准定位"该改哪个文件、改哪一行",而不必在错误层次(如 Widget Component)里浪费排查时间。
栅格尺寸总览:三层职责划分
Dashboard 的排版由三层各司其职:
DASHBOARD_WIDGET_CATALOG[].defaultLayout ← 你编辑的对象(默认尺寸 + 缩放下限)
↓
tileLayouts.ts :: calculateLayouts() ← 按 tile.widget.widget_type 读取 catalog
↓
react-grid-layout minW / minH on each item ← 编辑模式下缩放时被强制约束
几个关键约定(全部可在源码中验证):
- 栅格固定 12 列:
w/minW的单位是列数。前端BREAKPOINT_COLUMN_COUNTS.sm与后端 constants.py 中的DASHBOARD_GRID_COLUMN_COUNT = 12保持一致,文件头还专门注释了"Keep in sync with frontend BREAKPOINT_COLUMN_COUNTS.sm"。 - 高度用行单位:
h/minH不是像素,而是"栅格行"数。用户说"3 行高"即 3 个 grid rows。 - 行高常量:
BASE_ROW_HEIGHT = 80(px),定义在 DashboardItems.tsx,同处还有BASE_MARGIN = [16, 16]、CONTAINER_PADDING = [0, 0]。因此minH: 3意味着最小高度约3 × 80 = 240px(另加 margin)。编辑模式下还会乘以layoutZoom得到effectiveZoom(见DashboardItems.tsx中rowHeight = BASE_ROW_HEIGHT * effectiveZoom)。
宽表格(如列表类 widget)在
WidgetCardContent内部横向滚动,而不是让 dashboard 栅格整体横向滚动——这是内容层与栅格层的边界划分。
Tile 最小/最大尺寸:唯一的权威修改入口
改动 widget 最小尺寸的正确位置是 catalog + tileLayouts.ts,而不是 widget 的 Component。Agent 或开发者经常第一个就找错层(在组件 SCSS 里调高度),请直接使用本小节。
单一事实来源(Single Source of Truth)
在 catalog 条目上设置最小尺寸,例如 catalog.ts 中 error_tracking_list 的写法:
// products/dashboards/frontend/widget_types/catalog.ts
defaultLayout: { w: 6, h: 5, minW: 3, minH: 3 },
| 字段 | 含义 |
|---|---|
w, h |
添加 widget 时的默认尺寸(dashboardLogic.addWidgetTiles 会读取它们) |
minW, minH |
在 dashboard 上缩放时允许的最小尺寸(下限) |
在 DASHBOARD_WIDGET_CATALOG 中,多数列表类 widget(error_tracking_list、session_replay_list、experiments_list、experiment_results、survey_results、activity_events_list、logs_list)均采用 { w: 6, h: 5, minW: 3, minH: 3 };而 conversations_recent_tickets 用 { w: 6, h: 6, minW: 3, minH: 4 }——这印证了"不同类型可以有不同下限"的设计。
最小尺寸如何到达栅格(传递链路)
- calculateLayouts(tiles) 在每次 dashboard 加载或 tile 变化时执行,为每个断点(
sm/xs,取自BREAKPOINT_COLUMN_COUNTS)计算布局。 - 对 widget tile,调用
getWidgetCatalogLayout(widget_type)(tileLayouts.ts)→ 返回DASHBOARD_WIDGET_CATALOG[key].defaultLayout;若 widget_type 缺失或未知则返回undefined,由调用方使用场景级回退。 getTileMinDimensions()(tileLayouts.ts)根据 tile 类型(image / text / button / widget / default)为每个布局项设置 RGL 的minW/minH。widget 类型优先取 catalog 值,缺失时回退到MIN_TILE_DIMENSIONS.widget。- 这些
minW/minH会随布局对象一并传给 react-grid-layout,在编辑模式缩放时强制生效。
同一文件的其余逻辑也值得了解:calculateLayouts 会先按 sm 排序建立 referenceOrder,xs 断点忽略存储布局、仅跟随 sm 顺序推导;对 y === Infinity 的"脏"tile 采用最低段贪心放置(lowestSegments),这与后端 widget_layouts.py 的 _pack_at_bottom 逐列放置算法互相镜像。
持久化与"无需迁移"的设计
持久化的 tile JSON(tile.layouts.sm)只存 x、y、w、h,不存 min。最小尺寸每次都从 catalog 重新计算,因此修改 catalog 的 minH 会立即作用于所有现存 dashboard,无需任何数据迁移——这是"catalog 是唯一权威"带来的关键红利。
回退值(catalog 省略 min 时)
当 catalog 条目没有给出 minW / minH 时,由 tileLayouts.ts 中的 MIN_TILE_DIMENSIONS 提供回退:
| 场景 / 概念常量 | 值 | 何时生效 |
|---|---|---|
Widget tile 无 minW(MIN_WIDGET_TILE_WIDTH_COLS) |
3 | widget tile,catalog 未定义 minW |
Widget tile 无 minH(MIN_WIDGET_TILE_HEIGHT_ROWS) |
4 | widget tile,catalog 未定义 minH |
Insight tile(MIN_TILE_HEIGHT_ROWS) |
2 | insight tile(default 类型) |
Text tile(MIN_TEXT_TILE_HEIGHT_ROWS) |
1 | text tile(含 image / button 为 1×2 / 1×1) |
踩坑提示:如果新 widget 类型忘了写 minH,用户只能缩到 4 行而不是 3 行。务必在 catalog 条目上显式设置 minH / minW。
哪些地方不控制 dashboard 最小尺寸
| 位置 | 为什么无关 |
|---|---|
Widget Component / SCSS |
只负责内容布局;外层 tile 尺寸由 RGL 决定 |
widget_catalog.py 的 get_default_widget_layouts() |
后端"添加"辅助函数——只返回 w / h,没有 min 概念(见 widget_layouts.py 中 defaults["sm"]["w"] 的用法) |
Storybook widgetCardStoryFixtures 的 TILE_HEIGHT |
只是 story 的固定画框,不是 RGL |
DashboardWidgetsOverview.stories.tsx 的高度 |
总览展示的缩放比例,与真实 min 无关 |
全局修改 MIN_WIDGET_TILE_HEIGHT_ROWS |
只影响未知 widget 类型;优先用每类型的 catalog minH |
变更检查清单(Change checklist)
- 在 catalog.ts 中按
widget_type修改defaultLayout.minH/minW。 - 扩展 tileLayouts.test.ts:为对应
widget_type增加参数化用例,断言expectedMinH/expectedMinW。仓库已有范例——error_tracking_list、session_replay_list均断言minH: 3, minW: 3,而unknown_widget断言回退minH: 4, minW: 3;测试同时校验sm与xs两个断点。 - 运行
hogli test frontend/src/scenes/dashboard/tileLayouts.test.ts。 - 在 dashboard 编辑模式下手动缩放该 tile,确认下限生效。
- 可选:添加一个
MinimumSizestory——画框高度取minH * 80px,并限制 demo 数据量以保证内容不溢出。
Storybook 与真实 dashboard 的关系
- Dashboard:min 从 catalog →
tileLayouts.ts→ RGL,是生产行为。 - Storybook tile stories:使用可选的固定画框(
widgetCardStoryFixtures或 story 级 wrapper)。MinimumSizestory 只是记录下限,不会改变生产行为——不要把 story 高度当成真实 min。
Add widget 弹窗:多选、批量接口与底部堆叠
- 标题为 Add widget;描述为 Bring context from your different PostHog products into one dashboard.
AddWidgetModal支持多选:可勾选一个或多个 catalog 变体,然后点击 "Add N widgets"(N>1 时dashboardLogic会 toastAdded N widgets)。- 前端动作链:
dashboardLogic.addWidgetTiles(dashboardLogic.tsx)→POST api/environments/:teamId/dashboards/:dashboardId/widgets/batch/,单次 1–10 个 tile(后端 constants.py 的MAX_WIDGETS_BATCH_SIZE = 10与弹窗限制对应)。
后端放置:锚定最高列、向下堆叠
后端放置由 widget_layouts.stack_widget_layout_at_bottom 实现(widget_layouts.py),核心算法在 _find_bottom_row_placement(L29-L53):
- 先按列计算每列底部(
max(y + h)),新 tile 放在y = max(heights),并锚定在最高的那列(tallest_column)上; - 为什么要锚定最高列?因为栅格渲染时会做纵向压缩(vertical compaction)——只有列跨度覆盖了定义底部的列,tile 才能留在底部;否则会被压缩抬升到较矮列的空隙里(这就是历史上"落到第二行"类 bug 的成因);
- 批量添加向下堆叠:
pending_sm_layouts让第 2 个 tile 把第 1 个计入高度,依次往下排。注释明确说明"横向一行在阶梯状 dashboard 上无法在压缩中存活",所以批量为串行堆叠而非并排。
放置完成后,dashboardLogic 会调用 requestScrollToBottom() 将 #main-content 滚动到底部,保证新 tile 可见。
值得注意:前端支持"内联插入"(
pendingInsertion)——单 widget 添加时若用户指定了插入槽位,会带上layouts: { sm: { x, y, w, h } }走同一 batch 接口;多选添加因只 reposition 单个 tile,故不做内联插入,一律追加到底部(见 dashboardLogic.tsx)。
布局缺失 tile 的高度合成(防"掉进中部空隙")
后端放置会统计没有布局的 tile:collect_dashboard_sm_layouts_for_dashboard(widget_layouts.py)遍历 dashboard.tiles.exclude(deleted=True)(注意:deleted 为 NULL 的存活 tile 必须显式排除,因为 filter(deleted=False) 会漏掉 NULL),对每个 layouts = {} 的 tile 合成一个底部放置(默认 6×5,与前端 DEFAULT_INSERTED_TILE_SIZE = { w: 6, h: 5 } 对齐),让它们参与高度计算。
典型场景:通过 insight API 添加的 insight,其 layouts 为空,前端渲染时才临时排到底部、仅在保存布局时持久化。若后端只读持久化布局,就会低估看板高度,把新 widget 丢进页面中部的空隙——即注释中描述的 "lands in the 2nd row" bug。
REST / MCP 添加
REST 与 MCP 添加走 dashboard-widgets-batch-add,与前端共用同一个 batch 端点;单个 tile 即 widgets 数组的单个元素。细节见 mcp.md。
NEW 徽标(Add 菜单)
在 Add 菜单中把 Widget 入口标记为"新功能":
- 已填充的 dashboard:
DashboardHeaderActions中LemonMenu项加tag: 'new'。 - 空 dashboard:
EmptyDashboardComponent中,在 Widget 菜单项旁内联<LemonTag type="success" size="small">NEW</LemonTag>。
注意:不要给 AddWidgetModal 内的单个 catalog 变体加 NEW 徽标——徽标只属于菜单入口,不属于弹窗内的条目。
配置更新(Config updates)
运行时配置采用 PATCH 流程并带有保存守卫,详见 managing-existing-widgets.md § Config update flow;编辑弹窗的字段布局见同文档 § Edit modal layout。
移除与撤销(Remove and undo)
- 移除无确认弹窗,直接执行;通过 undo toast 提供反悔入口。
- 相关逻辑在
dashboardLogic.tsx的removeTileSuccess:cache.removedTileForUndo缓存被移除的 tile(dashboardLogic.tsx),撤销时取回。 - Toast 文案:"widget removed" + Undo。
⋯ 菜单对齐(DashboardWidgetItem)
每个 widget tile 的 ⋯ 菜单提供以下能力:
| 菜单项 | 行为说明 |
|---|---|
| View | 若配置了 titleHref(标题链接行为见 composition.md § Header title navigation) |
| Edit | 打开 widget 设置弹窗 |
| Duplicate | 复制 tile(前端 calculateDuplicateLayout 优先放右侧、空间不足则放下方,并下移可能重叠的 tile,见 tileLayouts.ts) |
| Show/hide description | 切换描述可见性;编辑文案在设置弹窗中完成 |
| Dashboard section | 复制 / 移动到另一个 dashboard、移除 |
| Refresh data | 直接点击刷新;可选带 "Last computed" 副标题(dashboard_tile 头布局;未知 widget 类型则省略) |
头布局选择(Header layout choice)
新 widget 优先使用 headerLayout: 'dashboard_tile'——与 insight tile 的"外壳"(chrome)一致:Refresh data 是 ⋯ 菜单的直接项(可选 "Last computed" 副标题),体验与 insight tile 对齐。
代码层面,catalog.ts 定义了 DASHBOARD_WIDGET_HEADER_LAYOUTS = ['simple', 'dashboard_tile'],且 DEFAULT_DASHBOARD_WIDGET_HEADER_LAYOUT = 'dashboard_tile'(satisfies 语法保证它是合法枚举值);headerMeta 默认 { showWidgetType: true, showDateRange: true },可被 catalog 条目覆盖(如 experiments_list、experiment_results 设 showDateRange: false,避免显示无意义的默认日期)。完整头布局细节见 composition.md。
配置中的日期范围(Date range in config)
时间区间存放在 config.dateRange 中,形状与 insight 一致:{ date_from, date_to?, explicitDate? }。
支持的相对 date_from 取值(由短到长),定义在后端 constants.py 的 WIDGET_DATE_FROM_VALUES_ORDERED 与 Pydantic WidgetDateFrom(widget_specs/common.py)中:
| 值 | 含义 | 值 | 含义 |
|---|---|---|---|
-1M |
Last minute | -7d |
Last 7 days |
-30M |
Last 30 minutes | -14d |
Last 14 days |
-1h |
Last hour | -30d |
Last 30 days |
-3h |
Last 3 hours | -90d |
Last 90 days |
-24h |
Last 24 hours |
注意 M 表示分钟、m 表示月(见 constants.py 注释,对应 posthog.utils 相对日期解析),widget 只接受这些预设相对区间,不接受任意 HogQL 日期字符串;date_from 可省略(即不限制起始)。后端测试 test_run_widgets.py 用参数化用例覆盖了各 widget 类型接受 -1h / -3h / -24h 短区间的校验。
代码生成:hogli build:openapi 重新生成 widget-date-from-options.json;前端 widgetConfigShared.ts 再导出 WIDGET_DATE_RANGE_SELECT_OPTIONS(含 label)并从生成的 Zod schema 推断值类型。
显示:WidgetCardHeader 读取 config.dateRange + catalog 的 headerMeta,用 dateFilterToText 格式化为可读文本(如 "Last 7 days")。
描述展示(Description display)
当 show_description 开启时,卡片头部在标题下方渲染 markdown 描述(WidgetCardHeader / CardMeta),容器样式为 max-h-24 overflow-y-auto——即最多约 6 行高、超出滚动,避免描述撑爆 tile 高度。
参考资源汇总
- 布局计算与 min 回退:tileLayouts.ts
- 行高与 margin 常量:DashboardItems.tsx
- Widget 目录(defaultLayout / headerMeta / titleHref):catalog.ts
- 添加 widget 的前端动作链:dashboardLogic.tsx
- 后端底部堆叠与布局合成:widget_layouts.py
- 后端日期/批量常量:constants.py
- 布局单测(含 min 参数化用例):tileLayouts.test.ts、后端放置测试:test_widget_layouts.py
- 同系列参考:architecture.md、composition.md、managing-existing-widgets.md、mcp.md
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust4.2 K634
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown300
jforgamejforgame是一个一站式游戏服务器开发框架。包含游戏服务器开发所需要的各种组件,比如网关,socket服务端与客户端,自定义高效消息编解码,游戏热更新,游戏通用工具等等。包含游戏服,跨服,匹配服,后台管理系统等实现,同时提供大量业务案例以供学习。亦可用于其他socket应用,例如及时聊天等。Java101
fizz-gateway-nodeAn Aggregation API Gateway in Java . FizzGate 是一个基于 Java开发的微服务聚合网关,是拥有自主知识产权的应用网关国产化替代方案,能够实现热服务编排聚合、自动授权选择、线上服务脚本编码、在线测试、高性能路由、API审核管理、回调管理等目的,拥有强大的自定义插件系统可以自行扩展,并且提供友好的图形化配置界面,能够快速帮助企业进行API服务治理、减少中间层胶水代码以及降低编码投入、提高 API 服务的稳定性和安全性。Java50
certd开源SSL证书管理工具;全自动证书申请、更新、续期;通配符证书,泛域名证书申请;证书自动化部署到阿里云、腾讯云、主机、群晖、宝塔;https证书,pfx证书,der证书,TLS证书,nginx证书自动续签自动部署JavaScript60
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python280