尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Langfuse Peek 视图表格状态管理:K/J 导航下的过滤器、排序、分页与搜索持久化机制

Langfuse Peek 视图表格状态管理:K/J 导航下的过滤器、排序、分页与搜索持久化机制 Langfuse Peek 视图表格状态管理K/J 导航下的过滤器、排序、分页与搜索持久化机制【免费下载链接】langfuse Open source AI engineering platform: LLM evals, observability, metrics, prompt management, playground, datasets. Integrates with OpenTelemetry, LangChain, OpenAI SDK, LiteLLM, and more. YC W23项目地址: https://gitcode.com/GitHub_Trending/la/langfuse导读Peek 视图是 Langfuse 数据表格Traces、Observations、Scores 等侧边栏内的快速预览面板用户按 K/J 键盘快捷键在相邻条目间跳转时面板内嵌套的表格组件会因itemId变化而整体重挂载remount若不加处理表格的筛选、排序、分页与搜索状态会全部丢失。本文以 peek/README.md 为骨架结合 peek.tsx、PeekTableStateContext.tsx 及各 peek-aware Hook 的源码实现完整讲解该状态管理系统的设计动机、面板外壳尺寸/展开/关闭、架构分层、PeekTableState数据结构、接入新表格的完整步骤以及状态生命周期与已知边界风险。读完本文你将能独立为 Langfuse 任意数据表格接入持久的 Peek 状态并理解其背后的Provider 不重挂载、内容按 key 重挂载核心模式。一、为什么需要 Peek 状态管理K/J 导航与重挂载问题Langfuse 的 Peek 视图允许用户在侧边面板中快速预览表格条目。当用户使用 K/J 快捷键在条目之间导航时peekURL 参数即itemId会变化TablePeekView内部以key{itemId}标识的内容区块随之重挂载// web/src/components/table/peek.tsx 中的核心结构 PeekTableStateProvider {/* ← 跨 itemId 变化保持挂载 */} div classNameflex-1 overflow-auto key{itemId} {children} {/* ← 只有这里会重挂载 */} /div /PeekTableStateProvider如果表格状态直接存放在组件本地或 URL query 中每次重挂载都会使过滤器、排序、分页、搜索全部归零。Peek 状态管理系统通过将表格状态提升到PeekTableStateProvider提供的 Context 中让状态实例在导航期间保持不变从而解决这一问题。从源码可以看到该 Provider 的初始状态定义PeekTableStateContext.tsxconst [tableState, setTableState] useStatePeekTableState({ filters: [], sorting: undefined, pagination: { pageIndex: 0, pageSize: 50 }, search: { query: null, type: [id] }, });二、面板外壳尺寸、展开与关闭TablePeekViewpeek.tsx是响应式的且始终保持 Peek悬浮在表格之上不拆分布局。2.1 桌面端右侧停靠的非模态 Dialog桌面端使用非模态modal{false}的 Radix DialogSheet没有遮罩层表格仍可交互面板左缘有一个可拖拽的 resize 手柄位于absolute inset-y-0 -left-1横跨面板左边缘左右两侧均可抓取拖到最右边缘或点击头部 Expand 按钮会展开到最大宽度视口宽度 − 侧边栏宽度侧边栏保持可见。展开状态由 URL 参数peekViewexpanded持有可分享、可刷新恢复在peek.tsx中通过router.replaceshallow写入关闭时由usePeekNavigation清理。替换而非压栈replace 而非 push是为了避免展开/收起切换刷爆浏览器历史。2.2 手持端vaul 底部抽屉当useIsHandheld判定为手持设备时宽度小于md或在矮屏上使用粗指针设备——例如横屏手机Peek 渲染为vaul底部抽屉支持原生下滑手势关闭此时隐藏 Expand 按钮。值得注意的是该判定不是只按宽度横屏手机宽度超过md但仍应得到抽屉而非桌面 Sheet这正是引入useIsHandheld的原因。Provider 同时包裹移动端与桌面端两种外壳位于isHandheld分支之上因此当用户跨断点调整窗口大小时抽屉↔Sheet 的切换不会重挂载 Provider已持有的表格状态得以保留——只有关闭 Peekreturn null才会卸载 Provider 并重置状态。2.3 关闭行为与例外规则关闭途径包括点击外部、Escape 键、关闭按钮、移动端下滑。但点击外部关闭存在一系列精心设计的例外shouldKeepPeekOpenOnOutsideInteraction场景行为依据点击 Peek 内部[data-peek-content]不关闭防内部分割条手柄被 Radix 误报为外部点击另一表格行[data-row-index]就地切换peek 条目不关闭行的自有点击处理器负责勾选选择复选框[rolecheckbox]不关闭全局常量ALWAYS_KEEP_PEEK_OPEN_SELECTORSdata-ignore-outside-interaction区域不关闭复用 outside-interaction 工具Toast 层覆盖[data-layertoast]如版本更新横幅不关闭关闭 Toast 不等于关闭 Peek表格自定义ignoredSelectors不关闭由表格透传保护行内操作按钮此外Peek 内打开的嵌套 Radix Popover/Menu 不会关闭 Peek依赖 RadixDismissableLayer的层级堆叠机制且onFocusOutside被显式preventDefault()——焦点移出例如焦点进入 portal 化的 Popover不会触发关闭只有指针、Escape 或关闭按钮驱动关闭。2.4 宽度状态的三层海拔面板状态遵循frontend-large-feature-architecture的 local-feature-state 模式将不同频率的状态放在不同海拔状态海拔存放位置expanded展开路由级可分享、可刷新URLpeekViewexpandedpeek.tsx 管理关闭时由usePeekNavigation清理width宽度跨视图持久化偏好storewidthFraction镜像到localStoragekey 为peekViewWidthFractionresize 拖拽高频瞬态storedraftFraction/draftExpanded/isResizingpointer-up 时提交当前条目item路由级peekURL 参数store/peekPanelStore.ts每次挂载创建一个 vanilla Zustand store懒useState持有只管理面板宽度 瞬态拖拽状态通过具名actions变更selectWidgetWidth、selectIsResizing、selectDraftExpanded等选择器返回原始值让订阅可以廉价地 bail outselectWidgetWidth返回50vw这样的 CSS 字符串宽度不变就不触发重渲染。actions/resizePeekPanel.ts完整拖拽工作流window 级 pointer 监听 → store actionspointer-up 时提交宽度或翻转展开标志。它不是 Hook直接接收 store 实例。值得注意的实现细节pointercancel被视为abort系统手势抢占、掌托误触、无障碍工具、pointer-capture 转移等只清理而不提交防止被取消的手势覆盖已持久化的宽度拖过expandAtFraction侧边栏边缘 viewport − sidebar动态传入就进入 expanded 预览startExpanded参数保证按下手柄本身不改变宽度只有移动指针才生效避免展开态下按下即回跳。usePeekPanelState.ts集成边界。持有 store、推导最终宽度widget vs expanded——展开时实测侧边栏偏移、把拖拽/键盘事件接到 store 与 action。pendingExpanded桥接异步间隙拖拽/按钮提交新 expanded 值后先本地持有直到 URLisExpanded追上避免松手后一帧旧宽度闪烁。侧边栏偏移通过ResizeObserver持续跟踪不止展开时才测量保证下次展开无陈旧偏移闪烁。键盘支持方向键微调左键放大、右键缩小步长 5%即KEYBOARD_RESIZE_STEP 0.05连续微调以 1 秒防抖合并为一次onResized通知。2.5 默认宽度视口感知的 px 上限默认宽度无保存偏好时为 50vw但用像素上限PEEK_MAX_DEFAULT_WIDTH_PX 1400封顶对应 LFE-10601普通笔记本仍按 50vw 打开只有超宽显示器会压缩比例保证 Peek 舒适且底层列表仍可导航。相关常量// web/src/components/table/peek/store/peekPanelStore.ts export const PEEK_MIN_WIDTH_FRACTION 0.4; // 最小 40vw export const PEEK_MAX_WIDGET_WIDTH_FRACTION 0.9; // 最大 90vw export const PEEK_DEFAULT_WIDTH_FRACTION 0.5; // 默认 50vw export const PEEK_EXPAND_ENTER_FRACTION 0.95; // 拖过 95vw 预览展开 export const PEEK_MAX_DEFAULT_WIDTH_PX 1400; // 默认宽度 px 上限resolveDefaultWidthFraction计算max(MIN, min(0.5, 1400 / vw))SSR 安全无 window 时返回纯比例。2.6 面板内部 tree↔info 分割Peek 内部的树/信息分割是独立的react-resizable-panels分组TraceLayoutDesktop与面板宽度统一持久化在localStorage中使用 peek 作用域的分组 idtrace-layout-peek-*而非按标签页隔离的sessionStorage。其默认值以显式百分比计算而非库默认的 pxdefaultSize——那会在中间态宽度上解析导致默认值不确定信息面板获得舒适的目标占比树/时间线取剩余部分并被夹在舒适区间内——因此大屏 Peek 的额外宽度流向 info内容而非 tree索引。全页 Trace 视图则保留自己按比例共享、按标签页隔离的布局。2.7 与全页 Trace 页面的共享Peek 与独立 Trace 页面已经共享一套 beta 感知的数据获取 Hook useTraceDetailData.ts、一个 body 标题TraceDetailBody.tsx 与 traceDetailTitle.ts、以及同一套操作集TraceDetailActions.tsx——star / publish / deleteusePeekData现在只是共享 Hook 的薄封装。README 还预告了下一步重构方向把Trace context的分支折叠进单一TraceDetailSurface包装器让 Peek 与TracePage共享一个组件而非四个。三、架构持久化 Provider 与重挂载内容的分离┌─────────────────────────────────────────────────────────────┐ │ TablePeekView (peek.tsx) │ │ │ │ PeekTableStateProvider ← Persists across itemId changes│ │ div key{itemId} ← Only this remounts │ │ {children} ← Tables remount here │ │ /div │ │ /PeekTableStateProvider │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌───────────────────────────────────────┐ │ Table Components (e.g., ScoresTable) │ │ │ │ Hooks automatically detect peek: │ │ • useOrderByState() │ │ • usePaginationState() │ │ • useFullTextSearch() │ │ │ │ Hooks requiring explicit wiring: │ │ • useSidebarFilterState() │ └───────────────────────────────────────┘ │ ▼ ┌────────────────────────────────────────┐ │ Hook reads from peek context: │ │ │ │ const peekContext usePeekTableState()│ │ if (peekContext) { │ │ useSidebarFilterState({ │ │ stateLocation: peekContext, │ │ context: peekContext, │ │ }) │ │ return peekContext.tableState.X │ │ } │ │ return urlState │ └────────────────────────────────────────┘3.1 PeekTableStateProvider位置contexts/PeekTableStateContext.tsx在 K/J 导航期间提供持久的状态存储存储 filters、sorting、pagination、search当itemId在 K/J 导航中变化时不重挂载Peek 内的表格可在 Hook 支持或调用方显式接线时从该 Context 读取状态。Context 值结构为{ tableState, setTableState }其中setTableState是标准的DispatchSetStateActionPeekTableStatevalue 经useMemo缓存避免无谓重渲染usePeekTableState()即useContext(PeekTableStateContext)。3.2 Peek-Aware Hooks大多数状态管理 Hook 会自动检测自身是否运行在 Peek 视图内并相应地读写状态useSidebarFilterState是例外必须由调用方显式接线。1.useSidebarFilterState过滤器位置web/src/features/filters/hooks/useSidebarFilterState.tsx需要显式hookOptions接线stateLocation: peekContext并传入context: usePeekTableState()不接线则使用 URL 或 session storage 状态而非 peek context。从源码看UseSidebarFilterStateOptions是一个判别联合类型useSidebarFilterState.tsxpeekContext分支要求context: PeekTableStateContextValueurlAndSessionStorage分支支持sessionFilterContextId防跨上下文串味另有url与memory两种模式默认值为{ stateLocation: urlAndSessionStorage }。Hook 内部通过stateLocationType决定取hookOptions.context还是 session storage并在 URL 编码超过MAX_URL_FILTER_QUERY_LENGTH约 16KB规避 431时回退到 session-storage 镜像。2.useOrderByState排序位置web/src/features/orderBy/hooks/useOrderByState.ts有 peek context 时返回 context 状态否则返回 URL 状态。源码关键点在排序的默认值语义sorting undefined时返回调用方传入的initialState即未显式排序用默认排序一旦用户修改或禁用排序该显式 peek-local 值才会被持久化见setState写入sorting: newSorting的分支。也就是说PeekTableState里sorting为undefined表示沿用默认这点在后续接口一节再次强调。3.usePaginationState分页位置web/src/hooks/usePaginationState.ts同时支持page/limit与pageIndex/pageSize两种格式通过函数重载与paramNames参数切换自动检测 peek context存在时使用之。源码实现usePaginationState.ts在 peek context 下pageIndex/pageSize分支返回 TanStack 的PaginationState并支持函数式 updaterpage/limit分支将 context 的 0 基pageIndex转成 1 基page。两种格式都只读peekContext.tableState.pagination写入时做{...peekContext.tableState, pagination: newValue}的不可变更新。4.useFullTextSearch全文搜索位置web/src/components/table/use-cases/useFullTextSearch.tsx同时处理搜索 query 与搜索类型searchType取值范围为TracingSearchTypeid | content | input | output。在非 peek 分支中有一个细节当搜索类型回到默认作用域id或空数组时会移除searchType参数而不是写出显式的?searchTypeid保证 URL 与保存视图与无作用域状态一致无论变更来自搜索栏还是旧工具栏。四、PeekTableState 接口PeekTableState接口定义了被持久化的全部状态PeekTableStateContext.tsxinterface PeekTableState { filters: FilterState; sorting: OrderByState | undefined; pagination: { pageIndex: number; pageSize: number }; search: { query: string | null; type: string[] }; }字段语义与默认值字段类型初始值说明filtersFilterState来自langfuse/shared[]侧边栏过滤器集合sortingOrderByState \| undefinedundefinedundefined表示使用useOrderByState传入的默认排序用户修改或禁用后才持久化显式值pagination{ pageIndex: number; pageSize: number }{ pageIndex: 0, pageSize: 50 }0 基页码 每页条数search{ query: string \| null; type: string[] }{ query: null, type: [id] }搜索词 搜索类型作用域默认按 id 搜索五、接入指南如何为一个新表格启用 Peek 状态持久化5.1 用 peek-aware Hook 替代直接 URL 状态管理分页——不要直接使用useQueryParams// ❌ 直接管理 URL query const [paginationState, setPaginationState] useQueryParams({ pageIndex: withDefault(NumberParam, 0), pageSize: withDefault(NumberParam, 50), }); // ✅ 使用 usePaginationState自动 peek-aware const [paginationState, setPaginationState] usePaginationState(0, 50);搜索——使用useFullTextSearch// ❌ 直接管理 URL query const [searchQuery, setSearchQuery] useQueryParam(search, StringParam); // ✅ 使用 useFullTextSearch自动 peek-aware const { searchQuery, setSearchQuery } useFullTextSearch();过滤器——必须显式接线useSidebarFilterStateconst peekContext usePeekTableState(); const queryFilterOptions: UseSidebarFilterStateOptions useMemo(() { if (peekContext) { return { loading: isSidebarFilterLoading, implicitDefaultConfig: DEFAULT_SIDEBAR_IMPLICIT_ENVIRONMENT_CONFIG, stateLocation: peekContext, context: peekContext, }; } return { loading: isSidebarFilterLoading, implicitDefaultConfig: DEFAULT_SIDEBAR_IMPLICIT_ENVIRONMENT_CONFIG, stateLocation: urlAndSessionStorage, sessionFilterContextId: projectId, }; }, [isSidebarFilterLoading, peekContext, projectId]); const queryFilter useSidebarFilterState( filterConfig, filterOptions, queryFilterOptions, );⚠️useSidebarFilterState不再在内部检测 peek context。只要表格可能渲染在PeekTableStateProvider内调用方就必须显式传stateLocation: peekContext与context否则过滤器会持久化到 URL 或 session 状态而不是内存中的 peek 状态。排序——useOrderByState已经 peek-aware无需改动const [orderByState, setOrderByState] useOrderByState({ column: createdAt, order: DESC, });5.2 内部工作原理统一的context 优先URL 兜底模式大多数 peek-aware Hook 内部遵循同一模式export const useSomeState () { const peekContext usePeekTableState(); // URL-based state (fallback) const [urlState, setUrlState] useQueryParam(...); if (peekContext) { // In peek view: read/write from context const value peekContext.tableState.someProperty; const setValue (newValue) { peekContext.setTableState({ ...peekContext.tableState, someProperty: newValue, }); }; return { value, setValue }; } // Not in peek view: use URL state return { value: urlState, setValue: setUrlState }; };注意两点实现共性其一写入一律采用展开 覆盖单字段的不可变更新保证不丢其他字段其二Hook 顶层先无条件调用 URL HookuseQueryParam等再按 context 决定返回值从而保证 Hook 调用顺序在开/关 Peek 之间保持稳定符合 React 规则。六、状态生命周期与边界情况6.1 何时持久化预期行为 ✓表格状态在同一个 Peek 视图内进行 K/J 键盘导航时持续保留1. Open trace T1 → apply filter to ScoresTable 2. Press K/J → navigate to trace T2 3. ScoresTable in T2 retains the filter ✓原因K/J 导航期间PeekTableStateProvider保持挂载只有key{itemId}的内容重挂载因此同一类型条目间用户的 filter/sort/pagination 偏好得以保留。已完整集成的表格包括Traces、Observations、Scores、Evaluators、Events以及 Eval Templates搜索现已 peek-aware。6.2 何时重置安全行为 ✓表格状态在Peek 视图关闭时重置1. Open trace T1 → apply filter 2. Close peek (X button/Escape/click outside) 3. Open observation O1 → fresh state ✓原因关闭 Peek 会移除peekURL 参数触发Sheet关闭并卸载SheetContent进而卸载PeekTableStateProvider销毁全部状态对应 peek.tsx 中if (!itemId || !mounted) return null;提前返回对 Provider 的卸载。同一机制还解释了usePaginationState等 Hook 在 peek 分支外使用useQueryParams的兜底——关闭后回到常规 URL 状态。6.3 已为未来预置的表格Peek-Aware Hook 已就位以下表格目前没有 Peek 视图但已改用 peek-aware Hook未来若添加 Peek 视图即可直接工作Sessions 表、Models 表、Score Configs 表。6.4 已知风险单个 Peek 视图内多张表格共享状态风险等级LOW理论边界情况问题同一 Peek 视图内的所有表格共享单一PeekTableState对象。若一个 Peek 视图包含多张相互独立的分页表格它们会共享 pagination/filter/sort 状态。Hypothetical: Trace peek with both Scores table AND Events table → Navigate to page 2 of Scores table → context.pagination { pageIndex: 1, pageSize: 50 } → Events table also shows page 2 ❌当前现实Peek 视图通常只含一张主表格例如 trace 详情中的 ScoresTable同一表格类型的多个实例如 trace 级 scores observation 级 scores有意共享状态表格使用disableUrlPersistence并通过 propstraceId、observationId作用域化数据。未来方案若确需多种独立表格类型任何为 peek 状态做命名空间namespaced的后续设计仍需保留显式的useSidebarFilterState接线模式const queryFilterOptions: UseSidebarFilterStateOptions peekContext ? { loading, stateLocation: peekContext, context: peekContext, } : { loading, stateLocation: urlAndSessionStorage, sessionFilterContextId: projectId, }; const filters useSidebarFilterState(config, options, queryFilterOptions);6.5 其他值得注意的生命周期细节删除竞态shouldClosePeekAfterDeleteLFE-10535——删除请求返回后仅当 Peek当前仍显示被删除的 trace 时才关闭。若用户先删除 trace A在 mutation 落定前又 K/J 跳到 trace B则必须保留 B 的 Peek否则 A 的陈旧closePeek回调会清掉现在的B 的 peek 参数。挂载门控首次渲染以mounted状态门控避免useIsHandheld解析完成前先绘制桌面 Sheet移动端深链会闪错外壳。data-peek-content守卫某些原生捕获指针的 primitive如内部react-resizable-panels的分割手柄可能绕过 Radix 的内部检测而被误报为外部该守卫保证 Peek 内的交互不会在中途关闭并卸载面板分组。七、相关文件速查用途路径Peek 状态 ContextProvider usePeekTableStatecontexts/PeekTableStateContext.tsxPeek 视图外壳桌面 Sheet / 移动 Drawer、点击外部规则peek.tsx面板宽度 storevanilla Zustandstore/peekPanelStore.ts拖拽 resize 工作流actions/resizePeekPanel.ts面板宽度集成边界 HookusePeekPanelState.tsPeek 打开/关闭与 URL 参数管理hooks/usePeekNavigation.ts分页 Hookweb/src/hooks/usePaginationState.ts全文搜索 Hookuse-cases/useFullTextSearch.tsx过滤器 Hookweb/src/features/filters/hooks/useSidebarFilterState.tsx排序 Hookweb/src/features/orderBy/hooks/useOrderByState.ts共享数据获取Peek 与全页共用web/src/features/traces/hooks/useTraceDetailData.ts共享 body / 标题 / 操作集TraceDetailBody.tsx、traceDetailTitle.ts、TraceDetailActions.tsx结语Langfuse 的 Peek 状态管理系统用Provider 持久 内容按 key 重挂载这一简洁模式解决了 K/J 快捷导航下表格状态丢失的体验问题并把四种状态过滤器/排序/分页/搜索 三档状态海拔路由/持久化偏好/瞬态拖拽清晰拆解到各自归属的层。对开发者而言接入新表格只需遵循优先使用 peek-aware Hook、为useSidebarFilterState显式接线两条铁律对架构师而言本地 store 命名 action 共享数据层折叠TraceDetailSurface的方向也提供了很好的参考。相关实现与测试如 resizePeekPanel.clienttest.ts、peekPanelStore.clienttest.ts、outsideInteraction.clienttest.tsx均在仓库web/src/components/table/peek/目录下可进一步深入研读。【免费下载链接】langfuse Open source AI engineering platform: LLM evals, observability, metrics, prompt management, playground, datasets. Integrates with OpenTelemetry, LangChain, OpenAI SDK, LiteLLM, and more. YC W23项目地址: https://gitcode.com/GitHub_Trending/la/langfuse创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表