关于 app-studio-build
app-studio-build 是通过 community-domo-cli 创建和组装 Domo App Studio 应用的技能,面向构建数据仪表板的用户。它涵盖页面、指标卡、图表、筛选器、布局与导航配置。
主要功能
- 01创建应用与页面
- 02配置主题和图标
- 03构建 KPI 指标卡与图表
- 04添加页面筛选器
- 05编排卡片布局与导航
适用场景
- 01创建含多页面的数据仪表板
- 02为页面配置指标和数据可视化
- 03跨页面共享筛选器并安排卡片
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ stahura/ domo-ai-vibe-rules --skill app-studio-build安装后在 Claude Code 中输入/app-studio-build确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
layout-builder.pyREADME.mdlayouts/layout-a-right-sidebar.mdlayout-b-symmetric-grid.mdlayout-c-left-column-feature.mdlayout-d-full-canvas.mdlayout-default-hero-grid.mdlayout-e-left-filter-form.md详细介绍
App Studio 构建编排器
构建完整的 App Studio 应用,包含原生卡片、核心指标、筛选器和精致布局。所有操作均使用 community-domo-cli。
有关详细信息,请委托以下技能:
card-creation— 卡片主体架构、图表类型索引、特定图表覆盖项的参考文件
前提条件
开始前,你需要:
- 每张卡片的数据集 GUID。发现列:
community-domo-cli --output json datasets schema $DATASET_ID > schema.json
- 页面定义 — 名称、图标、每个页面使用的数据集
- 核心指标定义 — 使用哪些列、聚合方式和对比周期
- 图表选择 — 每个页面使用哪种
badge_*类型(各页面类型应有所不同)
步骤 1:创建应用和页面
创建应用
community-domo-cli --output json -y app-studio create \
--body '{"title": "APP_TITLE", "description": "APP_DESCRIPTION"}' > app.json
APP_ID=$(python3 -c "import json; print(json.load(open('app.json'))['dataAppId'])")
LANDING_PAGE=$(python3 -c "import json; print(json.load(open('app.json'))['landingViewId'])")
创建其他页面
每个页面调用一次。落地页已存在 — 如有需要,可重命名。
python3 -c "
import json
app = json.load(open('app.json'))
owner_id = app['owners'][0]['id']
body = {
'owners': [{'id': owner_id, 'type': 'USER', 'displayName': None}],
'type': 'dataappview', 'title': 'PAGE_NAME', 'pageName': 'PAGE_NAME',
'locked': False, 'mobileEnabled': True, 'sharedViewPage': True, 'virtualPage': False
}
json.dump(body, open('view_body.json', 'w'))
"
community-domo-cli --output json -y app-studio create-view $APP_ID \
--body-file view_body.json > view_PAGE_NAME.json
PAGE_ID=$(python3 -c "import json; print(json.load(open('view_PAGE_NAME.json'))['view']['pageId'])")
每个页面都重复此操作。记录所有页面 ID。
步骤 2:主题和图标
应用主题
community-domo-cli --output json app-studio get $APP_ID > app_full.json
修改响应中的 theme。主题关键规范:
theme = app['theme']
# Zero-chrome mandatory standards
# theme keys are plural arrays — iterate, don't index directly
for comp in theme.get('components', []):
comp['borderRadius'] = 0
comp['borderWidth'] = 0
comp['dropShadow'] = 'NONE'
comp['itemBorderRadius'] = 0
if isinstance(comp.get('padding'), dict):
comp['padding'] = {'left': 0, 'right': 0, 'top': 0, 'bottom': 0}
for tbl in theme.get('tables', []):
tbl['borderRadius'] = 0
for nb in theme.get('notebooks', []):
if 'borderRadius' in nb:
nb['borderRadius'] = 0
# isDynamic and density are set on the layout JSON in Step 6, not on the theme.
# Colors use a reference system — you cannot assign hex strings directly.
# theme['colors'] is an array of 60 color objects: {"id": "c1", "value": {"value": "#333333", "type": "RGB_HEX"}, ...}
# Color fields on components/pages hold references: {"value": "c56", "type": "COLOR_REFERENCE"}
# To change a color, look up the referenced ID and update its hex in theme['colors']:
#
# target_id = theme['components'][0]['backgroundColor']['value'] # e.g. 'c56'
# for color in theme['colors']:
# if color['id'] == target_id:
# color['value']['value'] = '#1A1A2E'
# break
#
# Skip color changes unless you know the target color ID.
写回:
community-domo-cli --output json -y app-studio update $APP_ID --body-file app_full.json
上传自定义图标(可选)
community-domo-cli --output json -y files upload --file-path icon.png > icon.json
DATA_FILE_ID=$(python3 -c "import json; print(json.load(open('icon.json'))['dataFileId'])")
在应用更新请求体中,将 iconDataFileId 和 navIconDataFileId 设为 DATA_FILE_ID。
步骤 3:核心指标
核心指标使用 badge_pop_multi_value。这是最容易出错的卡片类型 — 请严格遵循以下要求。
必需的订阅
{
"subscriptions": {
"big_number": {
"name": "big_number",
"columns": [{"column": "METRIC_COL", "aggregation": "SUM", "alias": "METRIC_NAME", "format": {"type": "abbreviated", "format": "#A"}}],
"filters": []
},
"main": {
"name": "main",
"columns": [
{"column": "DATE_COL", "mapping": "ITEM"},
{"column": "METRIC_COL", "mapping": "VALUE", "aggregation": "SUM"}
],
"filters": [],
"orderBy": [],
"groupBy": [{"column": "DATE_COL"}],
"fiscal": false, "projection": false, "distinct": false
}
}
}
必需的图表配置
{
"charts": {
"main": {
"component": "main",
"chartType": "badge_pop_multi_value",
"overrides": {},
"goal": null
}
}
}
核心指标的关键注意事项
| 规则 | 原因 |
|---|---|
| 每页 3-4 个核心指标,且必须是单行 | 绝不能超过 5 个,也不能分成 2 行 |
仅使用 "format": {"type": "abbreviated", "format": "#A"} | 其他格式类型(货币、百分比)会导致 400 错误 |
不要包含 dateGrain | 与订阅结构冲突 |
注意:
dateRangeFilter与time_period订阅(周期环比比较)会导致卡片创建端点返回 HTTP 400。在问题解决前,请省略两者。核心指标将以单值 KPI 指标呈现,不显示同比变化。
每个核心指标创建一张卡片:
community-domo-cli --output json -y cards create \
--body-file hero_metric.json --page-id $PAGE_ID > hero_card.json
步骤 4:原生图表
每个页面都要有一个主可视化图表(全宽),以及 2-6 张详细卡片。各页面应使用不同的图表类型 — 不要在所有页面上使用同一种类型。
图表选择策略
| 数据模式 | 推荐类型 |
|---|---|
| 时间趋势 | badge_two_trendline、badge_vert_area_overlay、badge_curvedline |
| 类别比较 | badge_vert_bar、badge_horiz_bar、badge_vert_stackedbar |
| 部分与整体 | badge_donut、badge_vert_100pct、badge_treemap |
| 排名 | badge_horiz_bar、badge_funnel、badge_bump |
| 相关性 / 散点 | badge_xybubble、badge_xy_line |
| 表格详情 | badge_flex_table、badge_basic_table |
| 组合图(柱形图 + 折线图) | badge_line_bar、badge_symbol_bar |
创建每张卡片
- 按照
card-creation技能(第 2 节:主体架构)构建卡片请求体,该技能与本文件位于同一技能目录 - 从图表类型索引(第 5 节)中选择
chartType - 阅读
card-creation/references/中的相关参考文件(该目录与本文件位于同一技能目录)。重要参考文件:bar.md、line.md、pie.md、period-over-period.md、selector.md、table.md。 - 通过 CLI 创建:
community-domo-cli --output json -y cards create \
--body-file chart_card.json --page-id $PAGE_ID > chart_response.json
主可视化图表:布局宽度设为 60。详细卡片:宽度设为 20 或 30(每行 2-3 张)。
步骤 5:筛选卡片
用于驱动页面级筛选的选择器。大多数筛选器使用 badge_dropdown_selector。
筛选卡片请求体
{
"definition": {
"subscriptions": {
"big_number": {
"name": "big_number",
"columns": [{"column": "FILTER_COL", "aggregation": "COUNT"}],
"filters": []
},
"main": {
"name": "main",
"columns": [{"column": "FILTER_COL", "mapping": "ITEM"}],
"filters": [],
"groupBy": [{"column": "FILTER_COL"}],
"orderBy": [], "fiscal": false, "projection": false, "distinct": false
}
},
"formulas": {"dsUpdated": [], "dsDeleted": [], "card": []},
"annotations": {"new": [], "modified": [], "deleted": []},
"conditionalFormats": {"card": [], "datasource": []},
"controls": [],
"segments": {"active": [], "create": [], "update": [], "delete": []},
"charts": {
"main": {"component": "main", "chartType": "badge_dropdown_selector", "overrides": {}, "goal": null}
},
"dynamicTitle": {"text": [{"text": "FILTER_COL", "type": "TEXT"}]},
"dynamicDescription": {"text": [{"text": "", "type": "TEXT"}], "displayOnCardDetails": true},
"chartVersion": "12", "inputTable": false, "noDateRange": false,
"title": "FILTER_COL", "description": ""
},
"dataProvider": {"dataSourceId": "DATASET_UUID"},
"variables": true, "columns": false
}
重要: 选择器的 big_number.columns 不能为空 — 请对筛选列使用 COUNT。列为空会导致 HTTP 400。
在第一个页面上创建,然后共享到其他页面:
# Create on first page
community-domo-cli --output json -y cards create \
--body-file filter.json --page-id $FIRST_PAGE > filter_resp.json
FILTER_ID=$(python3 -c "import json; print(json.load(open('filter_resp.json'))['id'])")
# Add to other pages (goes to appendix — layout-set moves it to canvas)
community-domo-cli -y pages add-card $OTHER_PAGE $FILTER_ID
步骤 6:布局组装
从下表中选择布局(默认使用 Hero Grid)。阅读对应的布局参考文件,了解完整的网格坐标、card_positions 和 special_entries。
布局索引
| 布局 | 文件 | isDynamic | 插槽 | 最适用于 |
|---|---|---|---|---|
| 默认:Hero 网格 | layouts/layout-default-hero-grid.md | False | 筛选器 + hero 卡片 + 主可视化 + 详情行 | 带 KPI 指标的标准仪表板 |
| A:右侧边栏 | layouts/layout-a-right-sidebar.md | True | 左侧宽栏 + 右侧窄栏,3 个区块并带分页符 | 主从布局、评论面板 |
| B:对称网格 | layouts/layout-b-symmetric-grid.md | False | 全宽→两栏→三栏→6 格网格→全宽,分隔线 | 数据密集型对比仪表板 |
| C:左侧栏特色布局 | layouts/layout-c-left-column-feature.md | True | 左侧窄栏堆叠 + 右侧宽栏,下方双面板 | 带特色可视化的 KPI 侧边栏 |
| D:全画布 | layouts/layout-d-full-canvas.md | True | 单个边缘到边缘的插槽 | 嵌入式应用、地图或全出血可视化 |
| E:左侧筛选器 + 表单 | layouts/layout-e-left-filter-form.md | False | 左侧筛选器 + 右侧宽栏、间隔区、表单 + 卡片、底部全宽区域 | 数据录入 / 交互式表单 |
构建前请先阅读布局参考文件。 其中包含可粘贴到
layout-builder.py或下方内联脚本中的准确card_positions、special_entries和is_dynamic值。
重要:
cards create --page-id会自动将卡片作为附录条目添加到布局的content数组中。 创建完所有卡片后,务必运行layout-get,以填充 content 数组。下方的 Python 构建器会修改这些已有条目,不会创建新条目。 如果在创建卡片前运行layout-get,content 数组将为空,无法进行定位。
community-domo-cli --output json app-studio layout-get $APP_ID $PAGE_ID > layout.json
默认 y 带网格模式(Hero 网格)
| 区带 | y | height | 内容 |
|---|---|---|---|
| 横幅 | 0 | 14 | 页面横幅图片(pro-code)或 HEADER |
| 筛选器 | 14 | 6 | 并排的筛选器卡片 |
| Hero 卡片 | 20 | 14 | 单行显示 3-4 个 hero 指标 |
| 区块标题 | 34 | 4 | HEADER 文本 |
| 主可视化 | 38 | 30 | 全宽图表(宽度 60) |
| 区块标题 | 68 | 4 | HEADER 文本 |
| 详情卡片 | 72 | 30 | 每行 2-3 张卡片(宽度 20 或 30) |
内容条目类型
| 类型 | 必填字段 |
|---|---|
CARD | contentKey、type: "CARD"、cardId、x、y、width、height、style: null、hideTitle: true/false、hideSummary: true、virtual: false、virtualAppendix: false |
HEADER | contentKey、type: "HEADER"、text、x、y、width、height、virtual: false、virtualAppendix: false |
PAGE_BREAK | contentKey、type: "PAGE_BREAK"、x、y、width、height: 0 — 视觉分页边界 |
SEPARATOR | contentKey、type: "SEPARATOR"、x、y、width、height — 水平分隔线 |
SPACER | contentKey、type: "SPACER"、x、y、width、height — 空白区域 |
FORM | contentKey、type: "FORM"、x、y、width、height — 嵌入式表单区域 |
卡片布局内容条目
{
"contentKey": 100,
"type": "CARD",
"cardId": CARD_ID_INT,
"x": 0, "y": 38, "width": 60, "height": 30,
"style": null,
"hideTitle": false,
"hideSummary": true,
"hideBorder": false,
"hideMargins": false,
"fitToFrame": false,
"virtual": false,
"virtualAppendix": false,
"children": null
}
Hero 卡片条目 — 附加标志:
{
"hideTitle": true, "hideSummary": true,
"hideBorder": true, "hideMargins": true, "fitToFrame": true,
"style": null, "height": 14
}
筛选器卡片条目 — 低矮样式:
{
"hideTitle": true, "hideSummary": true,
"hideBorder": true, "hideMargins": true, "fitToFrame": true,
"style": null, "height": 6
}
构建并应用布局
快捷方式: 将
layout-builder.py(与本文档位于同一技能目录)复制到工作目录, 在文件顶部填写card_positions、header_positions、special_entries和is_dynamic(从布局参考文件中复制这些值),然后运行:python3 layout-builder.py --layout layout_PAGE.json --out layout_PAGE_updated.json下方的完整内联脚本使用相同逻辑——如果需要自定义位置以外的内容,请使用它。
import json, copy
layout = json.load(open('layout.json'))
# cards create --page-id puts every card in the appendix as a content entry.
# Build a cardId → contentKey lookup so positions can be keyed by cardId.
card_to_key = {}
for c in layout['content']:
if c['type'] == 'CARD' and 'cardId' in c:
card_to_key[c['cardId']] = c['contentKey']
# ── Define positions keyed by CARD ID (not contentKey) ──────────────────────
# Standard grid: total width = 60. Compact grid: total width = 12.
# hero=True → hideTitle/hideSummary/hideBorder/hideMargins/fitToFrame all True, h=14
# filt=True → same flags True, h=6
card_positions = {
# CARD_ID: {'x': X, 'y': Y, 'w': W, 'h': H, 'cx': CX, 'cy': CY, 'cw': CW, 'ch': CH}
# Hero example: {'x': 0, 'y': 20, 'w': 15, 'h': 14, 'cx': 0, 'cy': 7, 'cw': 6, 'ch': 6, 'hero': True}
# Filter example: {'x': 0, 'y': 14, 'w': 20, 'h': 6, 'cx': 0, 'cy': 0, 'cw': 12, 'ch': 4, 'filt': True}
# Chart example: {'x': 0, 'y': 38, 'w': 60, 'h': 30, 'cx': 0, 'cy': 19, 'cw': 12, 'ch': 20}
}
# Headers have no cardId — find their contentKey from the existing content array.
# layout['content'] will have one HEADER entry auto-created (text="Appendix").
# Add more by inspecting contentKeys after layout-get.
header_positions = {
# CONTENT_KEY: {'text': 'Section Title', 'x': 0, 'y': 34, 'w': 60, 'h': 4, 'cx': 0, 'cy': 15, 'cw': 12, 'ch': 3}
}
# ── Build new arrays ─────────────────────────────────────────────────────────
new_content = []
std_template = []
cmp_template = []
# Preserve SEPARATOR (contentKey=0, template-only system entry — no matching content entry).
for entry in layout['standard']['template']:
if entry.get('contentKey', -1) == 0:
std_template.append({**entry, 'virtual': True, 'virtualAppendix': True})
for entry in layout['compact']['template']:
if entry.get('contentKey', -1) == 0:
cmp_template.append({**entry, 'virtual': True, 'virtualAppendix': True})
for c in layout['content']:
key = c['contentKey']
entry = copy.deepcopy(c)
if c['type'] == 'HEADER' and key in header_positions:
pos = header_positions[key]
entry.update({'text': pos['text'], 'x': pos['x'], 'y': pos['y'],
'width': pos['w'], 'height': pos['h'],
'virtual': False, 'virtualAppendix': False})
new_content.append(entry)
std_template.append({'contentKey': key, 'type': 'HEADER',
'x': pos['x'], 'y': pos['y'], 'width': pos['w'], 'height': pos['h'],
'virtual': False, 'virtualAppendix': False, 'children': None})
cmp_template.append({'contentKey': key, 'type': 'HEADER',
'x': pos['cx'], 'y': pos['cy'], 'width': pos['cw'], 'height': pos['ch'],
'virtual': False, 'virtualAppendix': False, 'children': None})
elif c['type'] == 'CARD' and c.get('cardId') in card_positions:
pos = card_positions[c['cardId']]
hero = pos.get('hero', False)
filt = pos.get('filt', False)
flags = {'hideTitle': hero or filt, 'hideSummary': True,
'hideBorder': hero or filt, 'hideMargins': hero or filt,
'fitToFrame': hero or filt}
entry.update({**flags, 'x': pos['x'], 'y': pos['y'],
'width': pos['w'], 'height': pos['h'],
'virtual': False, 'virtualAppendix': False})
new_content.append(entry)
std_template.append({**flags, 'contentKey': key, 'type': 'CARD',
'x': pos['x'], 'y': pos['y'], 'width': pos['w'], 'height': pos['h'],
'virtual': False, 'virtualAppendix': False, 'style': None, 'children': None})
cmp_template.append({**flags, 'contentKey': key, 'type': 'CARD',
'x': pos['cx'], 'y': pos['cy'], 'width': pos['cw'], 'height': pos['ch'],
'virtual': False, 'virtualAppendix': False, 'style': None, 'children': None})
else:
# Leave in appendix (test cards, cards not yet positioned, etc.)
entry.update({'virtual': True, 'virtualAppendix': True})
new_content.append(entry)
std_template.append({'contentKey': key, 'type': c['type'],
'x': 0, 'y': 0, 'width': 15, 'height': 14,
'virtual': True, 'virtualAppendix': True, 'style': None, 'children': None})
cmp_template.append({'contentKey': key, 'type': c['type'],
'x': 0, 'y': 0, 'width': 6, 'height': 14,
'virtual': True, 'virtualAppendix': True, 'style': None, 'children': None})
# ── Special entries (PAGE_BREAK, SEPARATOR, SPACER, FORM) ────────────────────
# Copy these from the layout reference file. Empty list for default hero grid.
special_entries = [
# {'type': 'PAGE_BREAK', 'x': 0, 'y': 51, 'w': 60, 'h': 0, 'cx': 0, 'cy': 51, 'cw': 12, 'ch': 0},
]
# Set from layout reference file. Default hero grid uses False.
is_dynamic = False
if special_entries:
all_keys = [c.get('contentKey', 0) for c in new_content]
all_keys += [e.get('contentKey', 0) for e in std_template]
next_key = max(all_keys) + 100 if all_keys else 900
for se in special_entries:
ck = next_key
next_key += 1
base = {'contentKey': ck, 'type': se['type'],
'virtual': False, 'virtualAppendix': False, 'children': None}
new_content.append({**base, 'x': se['x'], 'y': se['y'],
'width': se['w'], 'height': se['h']})
std_template.append({**base, 'x': se['x'], 'y': se['y'],
'width': se['w'], 'height': se['h']})
cmp_template.append({**base, 'x': se['cx'], 'y': se['cy'],
'width': se['cw'], 'height': se['ch']})
layout['content'] = new_content
layout['standard']['template'] = std_template
layout['compact']['template'] = cmp_template
layout['isDynamic'] = is_dynamic
json.dump(layout, open('layout_updated.json', 'w'))
canvas = sum(1 for c in new_content if not c.get('virtualAppendix'))
appx = sum(1 for c in new_content if c.get('virtualAppendix'))
print(f'Layout written: {canvas} canvas, {appx} appendix, isDynamic={is_dynamic}')
应用:
community-domo-cli --output json -y app-studio layout-set $APP_ID $PAGE_ID \
--body-file layout_updated.json
对每个页面重复上述步骤。
步骤 7:导航
为每个页面设置 LEFT 方向,并使用自定义图标。
community-domo-cli --output json app-studio get $APP_ID > app_nav.json
import json
app = json.load(open('app_nav.json'))
app['navOrientation'] = 'LEFT'
app['showDomoNavigation'] = False
app['showTitle'] = False
app['showLogo'] = False
# nav['icon'] and nav['title'] are always silently ignored by the API — do not set them.
# Page titles in the nav come from the pageName set at create-view time, not from this update.
# To control a page's nav label, set 'pageName' and 'title' in the create-view body.
# Preserve system items (SEARCH, FAVORITES, etc.)
# They appear in navigations[] with type != 'VIEW' — don't remove them
json.dump(app, open('app_nav_updated.json', 'w'))
community-domo-cli --output json -y app-studio update $APP_ID --body-file app_nav_updated.json
注意:
navOrientation、showDomoNavigation、showTitle、showLogo可通过app-studio update正确更新。此 API 会始终静默忽略导航条目的title和icon字段——请勿设置。页面导航标签取自create-view时设置的pageName。
已验证的 Domo 原生图标名称
Google Material 图标名称无法使用。请仅使用以下名称:
| 类别 | 图标 |
|---|---|
| 首页 | home |
| 仪表板 | analytics、pop-chart、chart-bar-vertical、select-chart、badge-layout-8 |
| 运营 | gauge、dataflow、cube-filled、completed-submissions |
| 质量 | certified、checkbox-marked-outline、check-in-icon、approval-center |
| 物流 | globe、data-app、local_shipping、warehouse、shopping_cart |
| 零售 | store、cube-filled、numbers、toolbox |
| 财务 | money-universal、money、benchmark、books、calculator |
| 人员 | people、person、person-card、person-plus |
| 时间 | clock、calendar-simple、calendar-time、alarm |
| AI | ai-chat、magic、wand、lightbulb、lightning-bolt |
| 设置 | controls、pages-gear、code-tags、pencil-box |
步骤 8:验证
所有页面完成后:
- 检查卡片数量:
for PAGE in $LANDING_PAGE $PAGE2 $PAGE3 $PAGE4; do
echo "Page $PAGE:"
community-domo-cli --output json pages list-cards $PAGE | python3 -c "
import json,sys
data=json.load(sys.stdin)
cards=data.get('cards', [])
print(f' {len(cards)} cards')
for c in cards: print(f' - {c[\"id\"]}: {c.get(\"title\",\"untitled\")}')
"
done
- 向用户报告:
- 应用 URL:
https://INSTANCE.domo.com/app-studio/APP_ID - 应用 ID 和所有页面 ID
- 每个页面的卡片数量
- 创建失败的任何卡片(附错误详情)
- 应用 URL:
全局 UI/UX 标准(强制)
这些标准适用于使用此技能构建的每个 App Studio 应用:
| 标准 | 值 |
|---|---|
borderRadius | 所有位置均为 0——卡片、表格、笔记本、组件、按钮、标签页 |
borderWidth | 所有卡片均为 0 |
dropShadow | 所有卡片均为 NONE |
padding | 所有卡片均为 0 |
isDynamic | false——固定宽度布局 |
density | {compact: 8, standard: 8} |
| 控件颜色 c8 | #2563BE |
| 主视觉区 | 单行,高度 14,最多 4 个,hideTitle/hideSummary/hideBorder/hideMargins/fitToFrame 均为 true |
| 筛选器 | 高度 6,style: null,所有隐藏标记均为 true |
| 绝不重复创建应用 | 重试时,复用现有的 appId |
navOrientation | LEFT |
showDomoNavigation | 导航位于 LEFT 时为 false |