首页
/ PostHog 仪表盘 Widget 布局与 UX 技术指南:Catalog 栅格体系、编辑交互与实战配置

PostHog 仪表盘 Widget 布局与 UX 技术指南:Catalog 栅格体系、编辑交互与实战配置

2026-09-09 19:04:00作者:昌雅子Ethen

PostHog 的 Dashboard Widget(仪表盘小组件)体系允许把 Error Tracking、Session Replay、Experiments、Surveys、Logs 等不同产品的上下文汇聚到同一个看板。本文以仓库内 .agents/skills/manage-dashboard-widgets/references/layout-and-ux.md 为骨架,结合 tileLayouts.tscatalog.tswidget_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 = 240 px(另加 margin)。编辑模式下还会乘以 layoutZoom 得到 effectiveZoom(见 DashboardItems.tsxrowHeight = BASE_ROW_HEIGHT * effectiveZoom)。

宽表格(如列表类 widget)在 WidgetCardContent 内部横向滚动,而不是让 dashboard 栅格整体横向滚动——这是内容层与栅格层的边界划分。

Tile 最小/最大尺寸:唯一的权威修改入口

改动 widget 最小尺寸的正确位置是 catalog + tileLayouts.ts,而不是 widget 的 Component。Agent 或开发者经常第一个就找错层(在组件 SCSS 里调高度),请直接使用本小节。

单一事实来源(Single Source of Truth)

在 catalog 条目上设置最小尺寸,例如 catalog.tserror_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_listsession_replay_listexperiments_listexperiment_resultssurvey_resultsactivity_events_listlogs_list)均采用 { w: 6, h: 5, minW: 3, minH: 3 };而 conversations_recent_tickets{ w: 6, h: 6, minW: 3, minH: 4 }——这印证了"不同类型可以有不同下限"的设计。

最小尺寸如何到达栅格(传递链路)

  1. calculateLayouts(tiles) 在每次 dashboard 加载或 tile 变化时执行,为每个断点(sm / xs,取自 BREAKPOINT_COLUMN_COUNTS)计算布局。
  2. 对 widget tile,调用 getWidgetCatalogLayout(widget_type)tileLayouts.ts)→ 返回 DASHBOARD_WIDGET_CATALOG[key].defaultLayout;若 widget_type 缺失或未知则返回 undefined,由调用方使用场景级回退。
  3. getTileMinDimensions()tileLayouts.ts)根据 tile 类型(image / text / button / widget / default)为每个布局项设置 RGL 的 minW / minH。widget 类型优先取 catalog 值,缺失时回退到 MIN_TILE_DIMENSIONS.widget
  4. 这些 minW / minH 会随布局对象一并传给 react-grid-layout,在编辑模式缩放时强制生效。

同一文件的其余逻辑也值得了解:calculateLayouts 会先按 sm 排序建立 referenceOrderxs 断点忽略存储布局、仅跟随 sm 顺序推导;对 y === Infinity 的"脏"tile 采用最低段贪心放置(lowestSegments),这与后端 widget_layouts.py_pack_at_bottom 逐列放置算法互相镜像。

持久化与"无需迁移"的设计

持久化的 tile JSON(tile.layouts.sm)只存 xywh,不存 min。最小尺寸每次都从 catalog 重新计算,因此修改 catalog 的 minH 会立即作用于所有现存 dashboard,无需任何数据迁移——这是"catalog 是唯一权威"带来的关键红利。

回退值(catalog 省略 min 时)

当 catalog 条目没有给出 minW / minH 时,由 tileLayouts.ts 中的 MIN_TILE_DIMENSIONS 提供回退:

场景 / 概念常量 何时生效
Widget tile 无 minWMIN_WIDGET_TILE_WIDTH_COLS 3 widget tile,catalog 未定义 minW
Widget tile 无 minHMIN_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.pyget_default_widget_layouts() 后端"添加"辅助函数——只返回 w / h,没有 min 概念(见 widget_layouts.pydefaults["sm"]["w"] 的用法)
Storybook widgetCardStoryFixturesTILE_HEIGHT 只是 story 的固定画框,不是 RGL
DashboardWidgetsOverview.stories.tsx 的高度 总览展示的缩放比例,与真实 min 无关
全局修改 MIN_WIDGET_TILE_HEIGHT_ROWS 只影响未知 widget 类型;优先用每类型的 catalog minH

变更检查清单(Change checklist)

  1. catalog.ts 中按 widget_type 修改 defaultLayout.minH / minW
  2. 扩展 tileLayouts.test.ts:为对应 widget_type 增加参数化用例,断言 expectedMinH / expectedMinW。仓库已有范例——error_tracking_listsession_replay_list 均断言 minH: 3, minW: 3,而 unknown_widget 断言回退 minH: 4, minW: 3;测试同时校验 smxs 两个断点。
  3. 运行 hogli test frontend/src/scenes/dashboard/tileLayouts.test.ts
  4. 在 dashboard 编辑模式下手动缩放该 tile,确认下限生效。
  5. 可选:添加一个 MinimumSize story——画框高度取 minH * 80 px,并限制 demo 数据量以保证内容不溢出。

Storybook 与真实 dashboard 的关系

  • Dashboard:min 从 catalog → tileLayouts.ts → RGL,是生产行为。
  • Storybook tile stories:使用可选的固定画框(widgetCardStoryFixtures 或 story 级 wrapper)。MinimumSize story 只是记录下限,不会改变生产行为——不要把 story 高度当成真实 min。

Add widget 弹窗:多选、批量接口与底部堆叠

  • 标题为 Add widget;描述为 Bring context from your different PostHog products into one dashboard.
  • AddWidgetModal 支持多选:可勾选一个或多个 catalog 变体,然后点击 "Add N widgets"(N>1 时 dashboardLogic 会 toast Added N widgets)。
  • 前端动作链:dashboardLogic.addWidgetTilesdashboardLogic.tsx)→ POST api/environments/:teamId/dashboards/:dashboardId/widgets/batch/,单次 1–10 个 tile(后端 constants.pyMAX_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 的高度合成(防"掉进中部空隙")

后端放置会统计没有布局的 tilecollect_dashboard_sm_layouts_for_dashboardwidget_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 入口标记为"新功能":

  • 已填充的 dashboardDashboardHeaderActionsLemonMenu 项加 tag: 'new'
  • 空 dashboardEmptyDashboardComponent 中,在 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.tsxremoveTileSuccesscache.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_listexperiment_resultsshowDateRange: false,避免显示无意义的默认日期)。完整头布局细节见 composition.md

配置中的日期范围(Date range in config)

时间区间存放在 config.dateRange 中,形状与 insight 一致:{ date_from, date_to?, explicitDate? }

支持的相对 date_from 取值(由短到长),定义在后端 constants.pyWIDGET_DATE_FROM_VALUES_ORDERED 与 Pydantic WidgetDateFromwidget_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 高度。

参考资源汇总

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
docsdocs
暂无描述
Markdown
900
5.83 K
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
927
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.94 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
603
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
396
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.04 K
527