netx/docs/reviews/workbench-custom-modules-plan-2026-10-13.md
oliver 2307f19779 Let users pick and order their workbench module tiles.
Per-user layout lives in the new app_user.prefs JSON column, read via
/v1/auth/me and written via PUT /v1/auth/me/prefs/workbench. The
workbench gets an in-place edit mode (gear -> drag to reorder, x to
remove, tray to add back, reset/done). Reset stores empty prefs so
registry changes still apply; ids of modules the user can't currently
see are kept on save.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 08:31:51 +08:00

7.8 KiB
Raw Blame History

工作台功能卡片自定义 — 实现方案(2026-10-13)

目标

工作台功能区(UME 同步、网元管理、网络管理、拓扑、WebCRT…)以后会越来越多,每个人关心的不一样。 让每个用户自己选择:显示哪些卡片、按什么顺序;设置跟着账号走(换电脑/换浏览器仍生效)。

现状

  • 卡片注册表:web/src/config/modules.ts:33 MODULES,按 section 分 monitoring / operations / system。
  • 过滤逻辑:web/src/features/workbench/WorkbenchPage.tsx:323 visibleBySection —— 只按权限(adminOnly / requiredScope)和 workbenchHidden 过滤。
  • 渲染:web/src/features/workbench/components/WorkbenchModules.tsx:23,operations 是大磁贴网格,system 是底部一排小链接。
  • 后端无任何“用户偏好”存储;AppUser(netx_api/models/auth.py:13)只有账号字段。

设计决定

项 决定 理由
可自定义范围 只做 operations 区的磁贴(含 monitoring 合并进来的 UME 同步)。system 区链接行保持固定 系统管理入口少且是管理类,隐藏后反而找不到;需求说的是“功能卡片”
存储位置 服务端,按用户:app_user.prefs JSON 列 “每个人不一样”= 按账号;localStorage 只按浏览器,换机就丢
本地缓存 同时写一份 localStorage(key 带 user id),首屏先用缓存渲染,避免卡片闪烁 首屏体验
数据结构 { order: string[], hidden: string[] },存的是“隐藏了哪些”而不是“显示哪些” 以后新增模块默认对所有人出现,不需要每人手动开启
新模块默认隐藏 ModuleDefinition 增加可选 defaultHidden?: boolean;不在 order 里的这类模块视为隐藏 给以后“小众功能”留开关
权限 没权限的模块永远不出现在卡片区,也不出现在“可添加”列表;偏好里残留的 id 静默忽略 偏好不能绕过权限
交互 卡片区右上角一个小齿轮 → 进入原地编辑模式:已显示卡片可拖拽排序、右上角 × 移除;下方出现“可添加”托盘(灰显卡片,点击加入);按钮只有「恢复默认」「完成」 和 iOS 主屏编辑一致,最少文案;不弹窗
拖拽实现 HTML5 原生 drag & drop,不引入新依赖 卡片数量小(<20),原生够用;项目里没有 dnd 库
保存时机 点「完成」时一次 PUT;失败则 toast 并保留编辑状态 避免拖动过程中频繁请求
全部隐藏 允许,但功能区显示一个空态(只有齿轮 + 一句“添加功能”) 不阻止用户,但要能找回

任务

后端

B1 — app_user.prefs 列

  • netx_api/models/auth.py:13 AppUser 加 prefs: Mapped[dict] = mapped_column(_JsonType, default=dict)。
  • 新迁移 alembic/versions/20261013_user_prefs.py,down_revision = "20260922_biz_state_queued",add_column("app_user", sa.Column("prefs", JSON, nullable=False, server_default="{}"))。

B2 — 读写接口(netx_api/auth_router.py,紧跟 api_me 之后,约 :273)

  • GET /v1/auth/me/prefs → {"prefs": {...}}
  • PUT /v1/auth/me/prefs/{key},body 为该 key 的值;key 白名单,目前只允许 workbench。
  • 校验 workbench:{order: list[str], hidden: list[str]},每个列表 ≤ 64 项、每个 id ≤ 64 字符、只允许 [a-z0-9-];其他字段丢弃。超限 422。
  • 只改自己的偏好(require_user),不需要额外 scope;不写审计(纯 UI 偏好,避免刷屏)。
  • /v1/auth/me 响应里顺带返回 prefs,前端登录后一次拿到,不多一次请求。

前端

F1 — 注册表扩展 web/src/config/modules.ts

  • ModuleDefinition 加 defaultHidden?: boolean。
  • 新增 export function customizableModules():operations + monitoring 中非 workbenchHidden 的模块(即当前 displaySections 里 operations 那组的来源)。

F2 — 偏好 hook 新建 web/src/features/workbench/useWorkbenchLayout.ts

  • 输入:权限过滤后的可用模块列表;输出:{ shown: ModuleDefinition[], available: ModuleDefinition[], save(order, hidden), reset() }。
  • 解析规则(纯函数 resolveLayout(all, prefs),单独导出便于测试):
    1. 先按 prefs.order 排已知 id,丢弃不存在/无权限的 id;
    2. 剩余模块按注册表顺序追加到末尾;
    3. 在 hidden 中的、或 defaultHidden 且不在 order 中的,归入 available。
  • 数据来源:AuthContext 的 me.prefs.workbench → 没有则 localStorage 缓存 netx.wb.layout.<userId> → 默认。
  • 保存:PUT 成功后更新 AuthContext 中的 prefs 和 localStorage。localStorage 读写全部 try/catch。

F3 — AuthContext web/src/features/auth/AuthContext.tsx:48

  • refreshMe / login 把响应里的 prefs 存进 state,并暴露 prefs 与 setPrefs。

F4 — 编辑模式 UI web/src/features/workbench/components/WorkbenchModules.tsx

  • WorkbenchPage.tsx:334 displaySections 的 operations 组改为用 useWorkbenchLayout().shown。
  • 卡片区右上角齿轮按钮(ConsoleIcon 加一个 settings 图标),aria-label = “自定义功能区”。
  • 编辑态:
    • 磁贴轻微抖动或描边(遵循 glass 风格:不发光,只用 --nx-line 边框)、右上角 × 按钮;
    • draggable,onDragStart/onDragOver/onDrop 重排本地草稿数组;
    • 下方“可添加”托盘:灰显磁贴,点击加入末尾;托盘为空时不显示;
    • 底部两个按钮:「恢复默认」(清空草稿为默认)、「完成」(保存);Esc = 放弃草稿退出。
    • 编辑态下点击磁贴不打开模块。
  • 非编辑态 shown 为空:显示空态 + “添加功能”按钮(直接进入编辑态)。
  • 样式写在 web/src/styles/pages/workbench.css,颜色只用现有 token(蓝黑 200–215°,无紫色)。

F5 — 文案 web/src/i18n/locales/{zh,en}/workbench.ts

  • 新增:customize(自定义功能区)、customizeDone(完成)、customizeReset(恢复默认)、customizeAvailable(可添加)、customizeEmpty(添加功能)、customizeSaveFailed(保存失败)。只这些,不加说明文字。

验收

ID 场景 期望
A1 新用户首次登录 卡片与改动前完全一致(顺序、数量)
A2 隐藏「拓扑」、把「网元管理」拖到第一位,点完成 立即生效;刷新、换浏览器登录同一账号仍是该布局
A3 用户 A 定制后,用户 B 登录 B 不受影响
A4 在 MODULES 新增一个模块 已定制过的用户自动在末尾看到它;若设了 defaultHidden 则出现在“可添加”里
A5 偏好里包含无权限模块 id(如去掉用户 ne:read 后) 不显示、不出现在可添加;不报错
A6 全部移除后完成 显示空态,点“添加功能”可恢复
A7 编辑中按 Esc / 保存失败(断网) Esc 恢复原布局;失败提示并停留在编辑态
A8 PUT /v1/auth/me/prefs/workbench 传非法 key、超长列表、非法字符 404 / 422
A9 视觉 编辑态无发光、无紫色,与现有 glass 风格一致

回归命令

alembic upgrade head
python -m pytest tests/test_auth.py tests/test_user_prefs.py -q
cd web && npm run lint && npm run build
  • 新增 tests/test_user_prefs.py:覆盖 B2 的读写、白名单、校验、用户隔离(A3、A8)。
  • resolveLayout 纯函数若项目有前端单测则补上(A1、A4、A5);没有则在验收时手测。

以后可扩展(本次不做)

  • 同一机制用 prefs.workbench.panels 控制右侧任务栏、网元/拓扑/调度三张统计卡的显隐。
  • 管理员给角色设默认布局(role_default_prefs),用户未定制时用角色默认。