
PostHog Dashboard Widget 组件组合模式基于WidgetCard薄壳的复合组件架构实战【免费下载链接】posthog:hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experiments, error tracking, logs, and more – capture all the context agents need to diagnose problems, uncover opportunities, and ship fixes. Steer it all from Slack, web, desktop, or the MCP.项目地址: https://gitcode.com/GitHub_Trending/po/posthog导读PostHog 的 Dashboard Widget仪表盘小部件体系允许用户把错误追踪、会话回放、实验、调查、日志等不同产品线的数据以「磁贴」形式聚合到同一张仪表盘上。WidgetCard是这一体系的容器基座它只负责承载装饰性缩放手柄、编辑态边缘覆盖层与 react-grid-layoutRGL注入的缩放节点不接收任何产品级 header/body 属性。本文以 composition.md 为骨架结合仓库前端源码完整讲解WidgetCard复合组件模式、DashboardWidgetItem生产调用点、header 布局体系、产品视觉一致性、Storybook 规范与设置弹窗settings modal的工程实现。读完后你将掌握如何在 PostHog 中新增一个 widget 类型并正确编排它的外壳、头部、正文、筛选条与编辑弹窗。WidgetCard 复合模式总览薄壳 调用处组合PostHog 仪表盘 widget 遵循 Quill 组件库的Card模式薄壳thin shell 在调用点callsite组合的复合子组件。WidgetCard本身只是「瓦片外壳」——装饰性缩放手柄、编辑态边缘覆盖层、RGL 插槽不暴露 header/body 属性。文件 / 导出职责WidgetCard薄瓦片外壳装饰性缩放手柄、编辑态边缘覆盖层、RGL 插槽。无产品级 header/body 属性WidgetCardHeader含内部 title/actions 辅助组件布局路由器simple与dashboard_tile两种布局导出widgetCardShouldHideMoreButtonWidgetCardBody.tsx正文插槽锁定/错误外壳状态。同时导出WidgetCardContent、WidgetCardBodyMessage、WidgetLoadingState、WidgetCardBodySkeleton、WidgetCardSharedPlaceholderBody公共/共享占位WidgetCardContent可滚动列 可选 footer列表/表格类 widget——来自WidgetCardBody.tsxWidgetCardBodyMessage空态 / 行内状态文本——来自WidgetCardBody.tsxWidgetLoadingState/WidgetCardBodySkeletonwidget 自有的加载 UI——来自WidgetCardBody.tsxDashboardWidgetItem生产调用点——组合 header body串联 ⋯ 菜单、编辑弹窗 portal、产品 RBAC 锁当hasProductAccess时挂载注册表TileFiltersRBAC 拒绝时隐藏public场景使用WidgetCardSharedPlaceholderBody而非真实 widget bodyWidgetCardHeaderDescription从WidgetCardHeader.tsx导出仅供测试使用。widgetComponent永不渲染卡片外壳只渲染正文内容原语WidgetCardContent、WidgetCardBodyMessage等。从源码看这一「薄壳」定位在 WidgetCard.tsx 中非常清晰WidgetCard通过forwardRef接收 RGL 注入的className/style渲染顺序严格固定为children组合好的 header body→DashboardResizeHandles当showResizeHandles→EditModeEdgeOverlay当启用边缘进入编辑态→gridChildrenRGL 的.react-resizable-handle节点与InsightCard保持一致。Compound pattern组合示例WidgetCard ref{ref} className{className} style{style} showResizeHandles{showResizeHandles} canEnterEditModeFromEdge{canEnterEditModeFromEdge} onEnterEditModeFromEdge{onEnterEditModeFromEdge} gridChildren{rglHandles} // react-grid-layout 注入的缩放节点 WidgetCardHeader layout{headerLayout} title{title} defaultTitle{defaultTitle} // …catalog 驱动的 header 字段 shouldHideMoreButton{widgetCardShouldHideMoreButton(placement, showEditingControls)} moreButtonOverlay{…} / WidgetCardBody locked{locked} error{error} WidgetComponent … / /WidgetCardBody /WidgetCardPublic / shared dashboard—— header 相同但 body 只有占位无run_widgets数据{showSharedPlaceholder ? ( WidgetCardSharedPlaceholderBody copy{headerCatalogEntry.sharedPlaceholder ?? DEFAULT_SHARED_DASHBOARD_WIDGET_PLACEHOLDER} / ) : ( WidgetCardBody locked{locked} error{error} … WidgetComponent … / /WidgetCardBody )}WidgetCard内部渲染顺序与InsightCard一致children—— 组合好的 header bodyshowResizeHandles时渲染DashboardResizeHandles启用边缘进入编辑态时渲染EditModeEdgeOverlaygridChildren—— RGL 的.react-resizable-handle节点生产调用点DashboardWidgetItemDashboardWidgetItemDashboardWidgetItem.tsx是生产环境唯一调用点。它以WidgetCard为最外层节点通过forwardRef把ref、className、style交给卡片根节点——这是 RGL 通过cloneElement注入布局样式的前提装饰性缩放手柄与 RGL 的.react-resizable-handle因此共享同一父节点。其核心编排逻辑DashboardWidgetItemContent包括头部从 catalog 解析headerCatalogEntry组合WidgetCardHeader把widget.config、headerMeta、TopHeading、description、isLive、refreshControl、moreButtonOverlay传入widgetCardShouldHideMoreButton(placement, showEditingControls)决定 ⋯ 菜单是否隐藏。正文!hasProductAccess时WidgetCardBody进入locked状态未知 widget 类型不传递run_widgets错误真实 widget body 被ErrorBoundary包裹feature: dashboard_widget、widget_type、tile_id作为异常上下文。TileFilters当hasProductAccess且 widget 可用且注册表存在TileFilters时在 header 与 body 之间挂载始终可见的筛选条RBAC 拒绝时隐藏无编辑权限时以DASHBOARD_WIDGET_TILE_FILTERS_READONLY_REASON作为disabledReason。编辑弹窗EditModal通过createPortal挂到document.bodyonSave回调把 config 名称 描述一次 PATCH。Do 与 Dont边界与约定Do保持WidgetCard为DashboardWidgetItem的最外层节点——RGL 通过ref在卡片根部注入className/style在DashboardWidgetItem或 story 包装器中组合WidgetCardHeaderWidgetCardBody而不是在WidgetCard内部组合通过gridChildren传递 RGL 手柄而非children让 widgetComponent依据loadingprop 自行决定骨架屏还是内容使用min-w-0overflow-auto链路WidgetCardContent本身已做纵向滚动将时间周期存于config.dateRange——选项、编辑弹窗与 header 展示统一读取layout-and-ux.md默认 header 布局为dashboard_tile由getDashboardWidgetCatalogEntry()提供仅当覆盖默认值时才在 catalog 条目上设置headerLayout/headerMeta仅在 widget 设置弹窗中编辑标题/描述EditWidgetModalTileDetailsSection——卡片 header 是只读展示Dont不要给WidgetCard重新加回 header/body 属性——那会破坏复合模式不要把加载占位放进WidgetCard外壳——会破坏缩放/空态行为不要把 widget body 内容放进gridChildren——RGL 独占该插槽只用于缩放手柄不要在 widget 组件内部复制 header、菜单、卡片外壳或筛选开关——limit/sort/test 账号放在编辑弹窗date/status/property 筛选放在磁贴条tile bar而非 ⋯ 菜单不要在registry.tsx注册却没有对应的DASHBOARD_WIDGET_CATALOG条目Header 布局simple与dashboard_tile两种布局定义在 catalog.tsDASHBOARD_WIDGET_HEADER_LAYOUTS、DEFAULT_DASHBOARD_WIDGET_HEADER_LAYOUT。布局行为simple单标题行——新类型优先用dashboard_tile。仅通过磁贴 ⋯ 菜单刷新dashboard_tile紧凑CardMeta风格类型 • 日期范围 标题 分隔线刷新在 ⋯ 菜单中日期范围展示由WidgetCardHeader从config.dateRange 已解析的 catalogheaderMeta派生通过dateFilterToText格式化。产品类型芯片文本来自getDashboardWidgetGroupLabel(groupId)见 catalog.ts 的DASHBOARD_WIDGET_GROUP_LABELS。源码细节在 WidgetCardHeader.tsx 中showWidgetType与showDateRange均默认取headerMeta的配置dashboard_tile布局渲染紧凑的CardMetacompact描述以max-h-24 overflow-y-auto折叠展示simple布局则渲染传统 header 区块p-4 pb-2并内置拖拽手柄drag-handle cursor-move无编辑控件时隐藏。widgetCardShouldHideMoreButton的实现只有一行当 placement 为Public或showEditingControls false时隐藏 ⋯ 菜单。当保存视图或产品集合替换了日期范围时需要在widgets/registry.tsx中添加TopHeading插槽该插槽渲染CardTopHeadingRow携带组标签与解析后的保存视图名称复用产品项目的项目级保存视图缓存仅在需要持久化保存视图 ID 标签时加载同时把插槽加入 widget 的 Storybook 包装器并测试解析后的 heading。Loading ownership加载态归属widgetComponent从 scene logic 接收loading必须提前返回WidgetLoadingState。外壳DashboardWidgetItem→ 组合后的WidgetCardBody不会为 widget body 内容显示骨架屏。对应源码WidgetLoadingState与WidgetCardBodySkeleton默认 4 行骨架定义在 WidgetCardBody.tsxErrorTrackingWidget在loading时以ErrorTrackingIssueListSkeleton填充WidgetCardContentErrorTrackingWidget.tsx即「产品组件决定骨架、外壳不越权」的典型落地。RGL 与溢出widget 内容位于组合后的WidgetCardBody内gridChildren仅供 react-grid-layout 的缩放手柄使用宽表格横向滚动发生在WidgetCardContent内部而非仪表盘网格WidgetCardBody根节点WidgetCardBody.tsx使用container/widget-card容器查询 flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden p-4 pt-2内部WidgetCardBodySlot把 flex 高度从卡片外壳传递到正文内容WidgetCardContent负责overflow-y-auto overflow-x-hidden从而把表格横向滚动约束在内容列内。产品视觉一致性Product visual parity当 widget 展示的是既有 PostHog 产品的数据既有groupId中的变体或已有 scene 的产品区域中的首个 widget时默认复用产品 scene 使用的同一套展示——列表行、卡片、空态文案、骨架屏、设置引导。仪表盘磁贴只是更小的视口用户应能认出这是与应用内一致的产品数据例如/error-tracking上的ErrorTrackingIssueList而不是 widget 专用的另起炉灶表格。以图表为主体的 body 不属于 widget——请使用 insight 磁贴architecture.md § Charts → insight tiles。在哪找组件Intake 应该已经跑过 repo discovery——优先使用那些组件路径。当工程师指明了 scene 路径、tab、Storybook story 或同类 widget 时从 intake 的product UI reference出发——不要猜测其他界面否则打开该产品的主 scene列表、概览或详情索引记录渲染主数据块的组件——通常位于products/product/frontend/components/或scenes/area/把它们导入products/dashboards/frontend/widgets/product/并在WidgetCardContent或产品 setup gate 包装器内组合已上线的参考实现ErrorTrackingWidget从products/error_tracking/frontend/导入ErrorTrackingIssueList、ErrorTrackingIssueListSkeleton与ErrorTrackingIngestionPrompt见 ErrorTrackingWidget.tsx。平台 chrome vs 产品 chrome层级归属磁贴 header、⋯ 菜单、缩放、编辑弹窗外壳DashboardWidgetCard、EditWidgetModal*行、空态、加载骨架、设置引导产品导入共享组件不要在 widget body 内复制产品菜单、筛选器或页面级 chrome——配置归属 widget 设置弹窗layout-and-ux.md。Charts不在 widget 范围内。时间序列、漏斗等图表可视化属于insight 磁贴而不是新的widget_type。不要把产品图表组件作为 widget 的主 body。Storybook 与评审填充了数据的 stories 应渲染相同的产品组件并携带真实的run_*载荷以便视觉评审捕捉与 scene 的漂移不确定时与产品 Storybook story 或 scene 并排对比。当 parity 不可行时在 PR 中说明原因例如 scene 是带行内筛选器的整页布局且没有抽离出的列表。优先把组件薄抽取到产品包中而不是做一次性 dashboard 专属展示——这样也能让下一个 widget 变体保持一致。Storybook 规范平台原语位于Dashboards/Dashboard Widgets/目录WidgetCard/——WidgetCard.stories.tsxheader body 组合模式见 WidgetCard.stories.tsxOverview/——DashboardWidgetsOverview.stories.tsx所有 catalog 类型共享框架/mockwidgetCardStoryFixtures.tsx。按类型的 storieswidgets/product/Component.stories.tsx位于Widget types/ //例如Error tracking/Top issues。Metatitle必须是字符串字面量匹配DASHBOARD_WIDGET_GROUP_LABELS[groupId]/labelCSF 拒绝动态 title用WidgetCardWidgetCardHeaderWidgetCardBody catalog header 元数据组合——参见ErrorTrackingWidget.stories.tsx产品设置态Kea seed helper 位于widgetCardStoryFixtures.tsxwithErrorTrackingProjectState等——不要从*.stories.tsx导出 decoratorStorybook 会把导出当作 story冻结日期在 storyparameters中展开widgetStorybookParameters并把widgetOverviewStoryFixtures.ts中的 fixture 时间戳对齐到WIDGET_STORYBOOK_MOCK_DATE使 TZLabel / 相对时间文案在视觉评审中保持稳定谨慎堆叠 decoratorstory 级withErrorTrackingProjectState(false)不能被 meta decorator 里 seedtrue覆盖未知 / 部署偏差的 widget 类型当前端 catalog 缺少某个widget_type部分部署、未 rebase 的栈时Header——tryGetDashboardWidgetCatalogEntrygetUnknownDashboardWidgetCatalogFallback保证标题与 ⋯ 菜单remove、duplicate、copy/move仍然可用Body——ErrorBoundary包裹DashboardWidgetItemBody后者调用getDashboardWidgetCatalogEntry抛错 → 完整错误 UIFetch 错误—— 对未知类型不要把run_widgets的 fetcherror传给WidgetCardBody⋯ 菜单中无 Refresh data 操作Analytics——getDashboardWidgetDefinition仍会按规范类型去重上报 PostHogcaptureException——请补上注册表条目Widget 设置弹窗settings modalLemonModal 每个Edit*WidgetModal各自的 section——没有共享包装器。复制EditErrorTrackingWidgetModal.tsx起步。路径职责EditWidgetModalTileDetailsSection.tsx磁贴名称/描述EditWidgetModalFiltersSubsection.tsx产品h5下的测试账号 limit/sorteditWidgetModalBuilders.ts共享 kea actionsbuildWidgetTileMetadataPatch——仅展开 actionsreducer 按 logic 内联edit*WidgetModalLogic.ts校验 保存监听器widgetConfigValidation.ts*WidgetConfigValidation.tsZod注册表parseConfigApiErrorwidgetFilters.tswidgetFilters持久化/HogQL编辑配置磁贴持久化/恢复 hookswidgetFiltersUi.tsx筛选 chips编辑流程widgetTileFiltersReadOnly.tsxWidgetTileFiltersBar 只读标签*WidgetTileFilters.tsx注册表TileFilters——始终可见的筛选条constants.tsFetch 错误文案、WIDGET_TILE_REFRESH_DEBOUNCE_MS、formatWidgetListCountFooterLemonModal … footer{/* Cancel Save with saveDisabledReason / saving */} div classNameflex flex-col gap-4 {showTileDetails ? ( EditWidgetModalTileDetailsSection tileName{tileName} tileDescription{tileDescription} defaultTitle{defaultTitle} saving{saving} setTileName{setTileName} setTileDescription{setTileDescription} / ) : null} {showTypeSettings ? ( {showTileDetails ? LemonDivider classNamemy-0 / : null} section classNameflex flex-col gap-3 h5 classNametext-sm font-semibold m-0 {getDashboardWidgetGroupLabel(error_tracking)} /h5 div classNameflex flex-col gap-4 EditWidgetModalFiltersSubsection titleIssue filters … TestAccountFilter … / {/* limit, sort —— property/date/status 筛选在磁贴条上不在此处 */} /EditWidgetModalFiltersSubsection div{/* Sorting subsection */}/div /div /section / ) : null} /div /LemonModal参考EditErrorTrackingWidgetModal.tsx、EditSessionReplayWidgetModal.tsx。不需要的 section 就省略——用showTileDetails/ 产品级 setup 标志等布尔值做门控。可筛选的列表类 widget磁贴筛选条、分页 footer、titleHref——见 list-widget-patterns.md。Kea 编辑弹窗逻辑从editWidgetModalBuilders.ts展开actionswidgetEditModalListFieldActions、widgetEditModalTileActions、widgetEditModalFilterTestAccountsActions每个 logic 文件中使用内联 reducers——不要展开widgetEditModal*Reducerskea typegen 会丢失 reducer 类型使用每类型*FieldErrors类型内联setFieldErrors、clearFieldError、activeFieldErrors与saveDisabledReasonsetOrderByaction 接受stringreducer 转为配置枚举类型submit监听器validate*WidgetConfigInput→onSave(config, buildWidgetTileMetadataPatch(...))—— 一次 PATCH 提交 config 名称 描述连接filterTestAccountsDefaultsLogic通过resolveWidgetFilterTestAccounts初始化filterTestAccounts配置校验每类型持久化配置 弹窗表单 schema 来自generated/widget-configs.zod.ts同目录的*WidgetConfigValidation.ts只做 API 错误解析复用widgets/widgetConfigValidation.ts的共享 HogQL helper——不要手写字段守卫在DASHBOARD_WIDGET_REGISTRY条目上注册parseConfigApiError通过registry.tsx中的parseDashboardWidgetConfigApiError→utils.ts的updateDashboardWidgetTile分发。配置/代码生成见 config-and-codegen.md代码分割与懒加载从 registry.tsx 的注释可以确认widget UI 是代码分割的——静态图中只保留配置错误解析器、类型与懒加载工厂登录页面不再急切下载每个 widget 的渲染器、编辑弹窗与磁贴筛选条每个 widget 的子树只在其磁贴真正渲染时加载并通过DashboardWidgetItem与WidgetCardHeader中的Suspense边界渲染。WidgetCardHeader的可选TopHeading与TileFilters同样以DashboardWidgetSlot形式在Suspense中渲染因此新增 widget 类型的静态导入成本趋近于零——这也反过来约束了「catalog 条目必须与注册表条目成对出现」的纪律。小结PostHog 的 widget 体系是一个「薄壳 调用点组合 注册表驱动」的三层架构WidgetCard只提供瓦片外壳与 RGL 协作WidgetCardHeader负责两种 header 布局的排版路由WidgetCardBody提供锁定/错误/加载/占位等状态原语而DashboardWidgetItem作为唯一生产调用点把它们与产品组件、RBAC 锁、TileFilters、编辑弹窗编排在一起。新增 widget 类型时遵循本文的 Do/Dont 清单、产品视觉一致性原则与 Storybook 规范即可保证新磁贴与既有 insight 磁贴在外观与交互上完全对齐。【免费下载链接】posthog:hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experiments, error tracking, logs, and more – capture all the context agents need to diagnose problems, uncover opportunities, and ship fixes. Steer it all from Slack, web, desktop, or the MCP.项目地址: https://gitcode.com/GitHub_Trending/po/posthog创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考