8.5 KiB
Raw Blame History

name description version tags
sage-bricks-app-patterns Use when building Sage Bricks apps. Copy Sage, don't invent. 1.0.0
sage
bricks
rbac
permissions
login
index.ui

Sage Bricks App Patterns

Standard patterns for building Sage-platform web applications. Reference: Sage (元境) at ~/work/repos/sage.

Golden Rule

Look at Sage first. Before inventing anything, check how Sage does it. Copy the pattern.

Index Page (index.ui)

Bricks .ui page, NOT static HTML. Pattern (copied from Sage ~/work/repos/sage/wwwroot/index.ui):

VBox (root, height:100%)
├── HBox (header, 48px) — brand, Filler, theme button, language urlwidget, user_panel urlwidget
├── HBox (main, css:filler)
│   ├── VBox (sidebar, dark bg, 220px) — Menu widget
│   └── VScrollPanel (id:xxx_main_content, css:filler) — urlwidget for dashboard

Header 三要素(参照 Sage

// Header HBox subwidgets:
[
  {"widgettype":"HBox","subwidgets":[brand text + subtitle]},
  {"widgettype":"Filler"},
  {"widgettype":"Button","id":"theme_toggle_btn","options":{"label":"🌓","width":"36px","height":"36px"}},
  {"widgettype":"urlwidget","options":{"url":"{{entire_url('i18n/language.ui')}}"}},
  {"widgettype":"urlwidget","options":{"url":"{{entire_url('/rbac/user/user_panel.ui')}}"}}
]

Key: user_panel.ui 自动处理 登录/注册/用户信息 三种状态。

Menu 控件(不要用 Tree

强制使用 widgettype: "Menu",参照 Sage global_menu.ui

{"widgettype": "Menu", "id": "xxx_menu", "options": {
    "items": [
        {"name": "dashboard", "label": "📊 概览", "icon": "", "url": "{{entire_url('/overview.ui')}}", "target": "app.xxx_main_content"},
        {"name": "group", "label": "📦 分组", "icon": "", "items": [
            {"name": "sub1", "label": "子项1", "url": "{{entire_url('/list/index.ui')}}", "target": "app.xxx_main_content"}
        ]}
    ]
}}

Menu target 三定律

  1. 必须用 app.xxx_main_contentbricks.getWidgetById 基于 DOM closest/querySelector,找不到兄弟节点,只能从 body 级别搜索
  2. target id 放 VScrollPanel 上(不能放 HBox 父容器,否则清空时会连带 sidebar 一起删掉)
  3. icon 不能用 emoji"icon":"📊" 会被 bricks 当作 URL 请求 /📊,触发 401。设为 ""

RBAC Permissions

Use two scripts (ref Sage):

  1. set_role_perm.py — idempotent: python set_role_perm.py <role> <path>
  2. setup_rbac_perms.sh — calls set_role_perm.py for all paths in tiers

Permission Tiers (严格边界)

层级 角色 范围
公开 any stats API, index.ui, menu.ui, 静态资源, login/register
登录 logined 所有 get_*.dspy 数据查询, CRUD 写操作

铁律any 只能放只读操作stats/get/list/index/menu绝不可放 create/update/delete/deploy/allocate/release。

CRITICAL: Wildcard Paths

略...(保留原内容)

Redis Cache

After DB permission changes: redis-cli FLUSHDB then restart service. RBAC 缓存不改不会生效。 Permissions for directories MUST use /**:

WRONG: /bricks/      → exact match only
RIGHT: /bricks/**    → matches all sub-paths

RBAC only does wildcard matching when path ends with ** or %. Trailing slash alone does NOT work as prefix match.

Redis Cache

After DB permission changes: redis-cli KEYS "rbac*" | xargs redis-cli DEL then restart service. redis-cli FLUSHDB also works but wipes ALL caches (safe on dev).

Login

WRONG: custom HTML modal, iframe, custom doLogin() RIGHT: urlwidget → user_panel.ui (handles everything automatically)

用户菜单user_menu.ui / usermenu.ui

用户头像菜单(👤)的菜单项聚合机制(参照 Sage

  • 应用级 wwwroot/user_menu.ui带下划线用户菜单的聚合入口Sage 平台自动收集各模块的 usermenu.ui 合并展示。
  • 模块级 wwwroot/usermenu.ui(无下划线):各模块在自己 wwwroot 下放这个文件,声明要加入用户菜单的菜单项。

即:应用放 user_menu.ui,模块想往用户菜单加项就在模块 wwwroot 放 usermenu.ui。实测存在:sage/wwwroot/user_menu.ui(应用级)+ accounting|rbac|unipay/wwwroot/usermenu.ui(模块级)。不要直接改 rbac 的 usermenu.ui——rbac 是通用模块,所有应用共用。

Build Script

# deploy index.ui
cp -f "${PKGDIR}/pccs/wwwroot/index.ui" "${CDIR}/wwwroot/index.ui"
# run permission setup
bash "${CDIR}/setup_rbac_perms.sh"

Pitfalls

  1. bricks.js 401 → permission needs /** wildcard + any role
  2. Login page redirects → check /rbac/user/* files have any
  3. 401 loop → check /appbase/menu.ui, /rbac/user/user_panel.ui have any
  4. Still shows static HTML → config indexes: ["index.ui", "index.html"]
  5. Don't reinvent → check Sage's index.ui and top.ui first
  6. Menu emoji 401icon 不能用 emoji会被当成 URL 请求 /📊),设为 ""
  7. CRUD 工具栏消失"tools": [] 空数组覆盖默认按钮,删除此字段
  8. 免登录可 CRUDany 权限只能给 stats/index.ui/menu.uiget_*.dspy 和 CRUD 必须 logined
  9. DSPY f-string → 禁止!用字符串拼接:'text: ' + str(n)
  10. DSPY 桩代码grep -rl "'status': 'ok'" pkgs/*/wwwroot/api/ 检测假实现
  11. admin orgid=nullget_userorgid() 返回 null → DSPY 拒绝访问
  12. 改 RBAC 不生效redis-cli FLUSHDB + 重启服务RBAC 有 Redis 缓存)
  13. Menu target 覆盖全页 → target id 必须在 VScrollPanel 上,不能在 HBox 父容器
  14. Text 控件用 text 不用 otext(非 i18nset_attrs() 渲染 this.textotext 只在 i18n:true 时经翻译转成 text。DSPY 动态返回 widget JSON 时写 {'text': '值'},写 {'otext': ...} 渲染空白
  15. 语言切换方法bricks.app.change_language('zh')(本版本在 app 上NOT Sage 的 this.change_language 在 Menu 上)。备用:bricks.app.i18n.change_lang(). 语言 menu.ui 的 Menu item script: "bricks.app.change_language('zh')"
  16. 概览/stats 动态页面 → 用一个 DSPY 直接返回完整 widget 树HBox/VBox 卡片),不要在 .ui 里用 urlwidget 加载子 DSPY子 DSPY 返回的 widget JSON 子内容渲染空白)
  17. rbac 是通用模块,不放应用专属能力 → rbac 的 usermenu.ui/userinfo.ui 等是 Sage 平台通用模块(所有应用共用),不要往里塞某个应用专属的菜单项/能力(如"绑定微信"。应用专属入口放应用自己的主菜单sidebar或模块自己的 usermenu.ui。

概览 stats DSPY 模式

单个 DSPY 查所有模块数据,返回卡片树:

def card(title, value, subtitle, bgcolor, color):
    return {'widgettype':'VBox','options':{'bgcolor':bgcolor,'padding':'16px','width':'25%','border':'1px solid '+color,'borderRadius':'8px'},
        'subwidgets':[
            {'widgettype':'Text','options':{'text':title,'cfontsize':0.7,'color':'#64748b'}},
            {'widgettype':'Text','options':{'text':value,'cfontsize':2,'fontWeight':'bold','color':color}},
            {'widgettype':'Text','options':{'text':subtitle,'cfontsize':0.7,'color':'#94a3b8'}}
        ]}

# 资源聚合:总数=SUM(所有子节点),已用=SUM(status='allocated' 的子节点)
async with DBPools().sqlorContext(get_module_dbname('pcpool')) as sor:
    pools = await sor.R('compute_pool', {})
    for p in pools:
        nodes = await sor.R('compute_node', {'pool_id': p.id})
        for n in nodes:
            total_cpu += int(getattr(n,'cpu_cores',0) or 0)
            if getattr(n,'status','')=='allocated': alloc_cpu += ...
return {'widgettype':'VBox','options':{...},'subwidgets':[ ...pool_cards... ]}

关键:模型去掉冗余 total_/allocated_ 字段,全部从子节点动态聚合。

CRUD 子表 (subtables)

父表 CRUD JSON 加 params.subtables,生成工具栏按钮,点选后弹窗显示子表(自动过滤外键):

"params": {
    "subtables": [{
        "field": "pool_id",          // 子表外键字段
        "title": "算力节点",
        "url": "{{entire_url('../compute_node_list')}}",
        "subtable": "compute_node"
    }]
}

子表 CRUD JSON 加 data_filter 让列表可被外键过滤。注意:entire_url 内用单引号'../xxx'),双引号会被 Jinja2 转义成 \\\" 导致 TemplateSyntaxError。加了子表后,左侧主菜单不再需要子菜单项。