8.5 KiB
| name | description | version | tags | ||||||
|---|---|---|---|---|---|---|---|---|---|
| sage-bricks-app-patterns | Use when building Sage Bricks apps. Copy Sage, don't invent. | 1.0.0 |
|
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 三定律:
- 必须用
app.xxx_main_content—bricks.getWidgetById基于 DOMclosest/querySelector,找不到兄弟节点,只能从 body 级别搜索 - target id 放 VScrollPanel 上(不能放 HBox 父容器,否则清空时会连带 sidebar 一起删掉)
icon不能用 emoji —"icon":"📊"会被 bricks 当作 URL 请求/📊,触发 401。设为""
RBAC Permissions
Use two scripts (ref Sage):
set_role_perm.py— idempotent:python set_role_perm.py <role> <path>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
- bricks.js 401 → permission needs
/**wildcard +anyrole - Login page redirects → check
/rbac/user/*files haveany - 401 loop → check
/appbase/menu.ui,/rbac/user/user_panel.uihaveany - Still shows static HTML → config
indexes: ["index.ui", "index.html"] - Don't reinvent → check Sage's index.ui and top.ui first
- Menu emoji 401 →
icon不能用 emoji(会被当成 URL 请求/📊),设为"" - CRUD 工具栏消失 →
"tools": []空数组覆盖默认按钮,删除此字段 - 免登录可 CRUD →
any权限只能给 stats/index.ui/menu.ui,get_*.dspy 和 CRUD 必须logined - DSPY f-string → 禁止!用字符串拼接:
'text: ' + str(n) - DSPY 桩代码 →
grep -rl "'status': 'ok'" pkgs/*/wwwroot/api/检测假实现 - admin orgid=null →
get_userorgid()返回 null → DSPY 拒绝访问 - 改 RBAC 不生效 →
redis-cli FLUSHDB+ 重启服务(RBAC 有 Redis 缓存) - Menu target 覆盖全页 → target id 必须在 VScrollPanel 上,不能在 HBox 父容器
- Text 控件用
text不用otext(非 i18n) →set_attrs()渲染this.text;otext只在i18n:true时经翻译转成 text。DSPY 动态返回 widget JSON 时写{'text': '值'},写{'otext': ...}渲染空白 - 语言切换方法 →
bricks.app.change_language('zh')(本版本在 app 上,NOT Sage 的this.change_language在 Menu 上)。备用:bricks.app.i18n.change_lang(). 语言 menu.ui 的 Menu itemscript: "bricks.app.change_language('zh')" - 概览/stats 动态页面 → 用一个 DSPY 直接返回完整 widget 树(HBox/VBox 卡片),不要在 .ui 里用 urlwidget 加载子 DSPY(子 DSPY 返回的 widget JSON 子内容渲染空白)
- 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。加了子表后,左侧主菜单不再需要子菜单项。