mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
feat(base): add +dashboard-block-batch-create shortcut for creating multiple dashboard blocks in one call
Introduces BaseDashboardBlockBatchCreate shortcut that accepts a JSON array of block specs and creates them sequentially via a single CLI invocation. Includes local data_config validation, compact output summary, dry-run support, unit tests, e2e dry-run test, and updated skill docs to prefer batch over repeated single-block creates when two or more blocks are needed.
This commit is contained in:
@@ -395,6 +395,73 @@ func TestBaseDashboardBlockExecuteCreate(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestBaseDashboardBlockExecuteBatchCreate tests the +dashboard-block-batch-create command.
|
||||
func TestBaseDashboardBlockExecuteBatchCreate(t *testing.T) {
|
||||
t.Run("creates blocks sequentially with compact output", func(t *testing.T) {
|
||||
factory, stdout, reg := newExecuteFactory(t)
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "POST",
|
||||
URL: "/open-apis/base/v3/bases/app_x/dashboards/dsh_001/blocks",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0,
|
||||
"data": map[string]interface{}{
|
||||
"block_id": "blk_stat",
|
||||
"name": "订单数",
|
||||
"type": "statistics",
|
||||
"data_config": map[string]interface{}{
|
||||
"table_name": "订单表",
|
||||
"count_all": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "POST",
|
||||
URL: "/open-apis/base/v3/bases/app_x/dashboards/dsh_001/blocks",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0,
|
||||
"data": map[string]interface{}{
|
||||
"block_id": "blk_line",
|
||||
"name": "月度趋势",
|
||||
"type": "line",
|
||||
"data_config": map[string]interface{}{
|
||||
"table_name": "订单表",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
blocks := `[
|
||||
{"name":"订单数","type":"statistics","data_config":{"table_name":"订单表","count_all":true}},
|
||||
{"name":"月度趋势","type":"line","data_config":{"table_name":"订单表","series":[{"field_name":"金额","rollup":"sum"}],"group_by":[{"field_name":"月份","mode":"integrated","sort":{"type":"group","order":"asc"}}]}}
|
||||
]`
|
||||
args := []string{"+dashboard-block-batch-create", "--base-token", "app_x", "--dashboard-id", "dsh_001", "--blocks", blocks}
|
||||
if err := runShortcut(t, BaseDashboardBlockBatchCreate, args, factory, stdout); err != nil {
|
||||
t.Fatalf("err=%v", err)
|
||||
}
|
||||
got := stdout.String()
|
||||
if !strings.Contains(got, `"created_count": 2`) || !strings.Contains(got, `"blk_stat"`) || !strings.Contains(got, `"blk_line"`) {
|
||||
t.Fatalf("stdout=%s", got)
|
||||
}
|
||||
if strings.Contains(got, "data_config") {
|
||||
t.Fatalf("batch output should stay compact, stdout=%s", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rejects invalid block data config", func(t *testing.T) {
|
||||
factory, stdout, _ := newExecuteFactory(t)
|
||||
args := []string{"+dashboard-block-batch-create", "--base-token", "app_x", "--dashboard-id", "dsh_001",
|
||||
"--blocks", `[{"name":"Bad","type":"column","data_config":{"series":[{"field_name":"金额","rollup":"COUNTA"}]}}]`,
|
||||
}
|
||||
err := runShortcut(t, BaseDashboardBlockBatchCreate, args, factory, stdout)
|
||||
if err == nil {
|
||||
t.Fatalf("expected validation error")
|
||||
}
|
||||
if got := err.Error(); !strings.Contains(got, "blocks[0].data_config") || !strings.Contains(got, "data_config 校验失败") {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestBaseDashboardBlockExecuteUpdate tests the +dashboard-block-update command.
|
||||
func TestBaseDashboardBlockExecuteUpdate(t *testing.T) {
|
||||
t.Run("update name and data-config", func(t *testing.T) {
|
||||
@@ -596,6 +663,20 @@ func TestBaseDashboardBlockDryRun_Create(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestBaseDashboardBlockDryRun_BatchCreate tests the +dashboard-block-batch-create --dry-run flag.
|
||||
func TestBaseDashboardBlockDryRun_BatchCreate(t *testing.T) {
|
||||
factory, stdout, _ := newExecuteFactory(t)
|
||||
blocks := `[{"name":"订单数","type":"statistics","data_config":{"table_name":"订单表","count_all":true}},{"name":"月度趋势","type":"line","data_config":{"table_name":"订单表","series":[{"field_name":"金额","rollup":"SUM"}],"group_by":[{"field_name":"月份","mode":"integrated","sort":{"type":"group","order":"asc"}}]}}]`
|
||||
args := []string{"+dashboard-block-batch-create", "--base-token", "app_x", "--dashboard-id", "dsh_1", "--blocks", blocks, "--user-id-type", "open_id", "--dry-run", "--format", "pretty"}
|
||||
if err := runShortcut(t, BaseDashboardBlockBatchCreate, args, factory, stdout); err != nil {
|
||||
t.Fatalf("err=%v", err)
|
||||
}
|
||||
got := stdout.String()
|
||||
if !strings.Contains(got, "POST /open-apis/base/v3/bases/app_x/dashboards/dsh_1/blocks") || !strings.Contains(got, "\"blocks\"") || !strings.Contains(got, "\"sequential\":true") || !strings.Contains(got, "open_id") {
|
||||
t.Fatalf("stdout=%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBaseDashboardBlockDryRun_Update tests the +dashboard-block-update --dry-run flag.
|
||||
func TestBaseDashboardBlockDryRun_Update(t *testing.T) {
|
||||
factory, stdout, _ := newExecuteFactory(t)
|
||||
|
||||
@@ -170,7 +170,7 @@ func TestShortcutsCatalog(t *testing.T) {
|
||||
"+form-questions-create", "+form-questions-delete", "+form-questions-update", "+form-questions-list",
|
||||
"+form-submit",
|
||||
"+dashboard-list", "+dashboard-get", "+dashboard-create", "+dashboard-update", "+dashboard-delete", "+dashboard-arrange",
|
||||
"+dashboard-block-list", "+dashboard-block-get", "+dashboard-block-get-data", "+dashboard-block-create", "+dashboard-block-update", "+dashboard-block-delete",
|
||||
"+dashboard-block-list", "+dashboard-block-get", "+dashboard-block-get-data", "+dashboard-block-create", "+dashboard-block-batch-create", "+dashboard-block-update", "+dashboard-block-delete",
|
||||
}
|
||||
if len(shortcuts) != len(want) {
|
||||
t.Fatalf("len(shortcuts)=%d want=%d", len(shortcuts), len(want))
|
||||
@@ -548,6 +548,19 @@ func TestBaseDashboardHelpGuidesAgents(t *testing.T) {
|
||||
"sequentially",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dashboard block batch create",
|
||||
shortcut: BaseDashboardBlockBatchCreate,
|
||||
wantTips: []string{
|
||||
"lark-cli base +dashboard-block-batch-create --base-token <base_token> --dashboard-id <dashboard_id> --blocks",
|
||||
"two or more blocks",
|
||||
"sequentially",
|
||||
"+table-list and +field-list",
|
||||
"not table_id or field_id",
|
||||
"dashboard-block-data-config.md as the SSOT",
|
||||
"intentionally compact",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dashboard block update",
|
||||
shortcut: BaseDashboardBlockUpdate,
|
||||
|
||||
189
shortcuts/base/dashboard_block_batch_create.go
Normal file
189
shortcuts/base/dashboard_block_batch_create.go
Normal file
@@ -0,0 +1,189 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package base
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
)
|
||||
|
||||
type dashboardBlockCreateSpec struct {
|
||||
Index int
|
||||
Body map[string]interface{}
|
||||
}
|
||||
|
||||
var BaseDashboardBlockBatchCreate = common.Shortcut{
|
||||
Service: "base",
|
||||
Command: "+dashboard-block-batch-create",
|
||||
Description: "Create multiple blocks in a dashboard sequentially",
|
||||
Risk: "write",
|
||||
Scopes: []string{"base:dashboard:create"},
|
||||
AuthTypes: authTypes(),
|
||||
HasFormat: true,
|
||||
Flags: []common.Flag{
|
||||
baseTokenFlag(true),
|
||||
dashboardIDFlag(true),
|
||||
{Name: "blocks", Desc: "JSON array of block objects: [{\"name\":\"...\",\"type\":\"statistics\",\"data_config\":{...}}]. Use @file.json for long dashboards.", Required: true},
|
||||
{Name: "user-id-type", Desc: "user ID type for user fields in filters: open_id / union_id / user_id"},
|
||||
{Name: "no-validate", Type: "bool", Desc: "skip local data_config validation"},
|
||||
},
|
||||
Tips: []string{
|
||||
`lark-cli base +dashboard-block-batch-create --base-token <base_token> --dashboard-id <dashboard_id> --blocks '[{"name":"Order Count","type":"statistics","data_config":{"table_name":"Orders","count_all":true}},{"name":"Monthly Sales","type":"line","data_config":{"table_name":"Orders","series":[{"field_name":"Amount","rollup":"SUM"}],"group_by":[{"field_name":"Month","mode":"integrated","sort":{"type":"group","order":"asc"}}]}}]'`,
|
||||
"Use this when creating two or more blocks in the same dashboard; the CLI calls the platform sequentially for you.",
|
||||
"Before creating data-backed blocks, use +table-list and +field-list to confirm real table and field names.",
|
||||
"data_config uses table and field names, not table_id or field_id.",
|
||||
"Read dashboard-block-data-config.md as the SSOT for chart templates, filters, metric rules, and type-specific fields; do not invent data_config from natural language.",
|
||||
"The output is intentionally compact: created_count plus each created block's index, block_id, name, and type.",
|
||||
},
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
_, err := parseDashboardBlockCreateSpecs(runtime)
|
||||
return err
|
||||
},
|
||||
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||||
specs, _ := parseDashboardBlockCreateSpecs(runtime)
|
||||
blocks := make([]interface{}, 0, len(specs))
|
||||
for _, spec := range specs {
|
||||
blocks = append(blocks, spec.Body)
|
||||
}
|
||||
params := map[string]interface{}{}
|
||||
if uid := strings.TrimSpace(runtime.Str("user-id-type")); uid != "" {
|
||||
params["user_id_type"] = uid
|
||||
}
|
||||
return common.NewDryRunAPI().
|
||||
POST("/open-apis/base/v3/bases/:base_token/dashboards/:dashboard_id/blocks").
|
||||
Params(params).
|
||||
Body(map[string]interface{}{"blocks": blocks, "sequential": true}).
|
||||
Set("base_token", runtime.Str("base-token")).
|
||||
Set("dashboard_id", runtime.Str("dashboard-id"))
|
||||
},
|
||||
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
return executeDashboardBlockBatchCreate(runtime)
|
||||
},
|
||||
}
|
||||
|
||||
func parseDashboardBlockCreateSpecs(runtime *common.RuntimeContext) ([]dashboardBlockCreateSpec, error) {
|
||||
pc := newParseCtx(runtime)
|
||||
items, err := parseJSONArray(pc, runtime.Str("blocks"), "blocks")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(items) == 0 {
|
||||
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--blocks must contain at least one block").WithParam("--blocks")
|
||||
}
|
||||
specs := make([]dashboardBlockCreateSpec, 0, len(items))
|
||||
for index, item := range items {
|
||||
obj, ok := item.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--blocks[%d] must be a JSON object", index).WithParam("--blocks")
|
||||
}
|
||||
body, err := buildDashboardBlockCreateBody(runtime, obj, index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
specs = append(specs, dashboardBlockCreateSpec{Index: index, Body: body})
|
||||
}
|
||||
return specs, nil
|
||||
}
|
||||
|
||||
func buildDashboardBlockCreateBody(runtime *common.RuntimeContext, obj map[string]interface{}, index int) (map[string]interface{}, error) {
|
||||
name, _ := obj["name"].(string)
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--blocks[%d].name is required", index).WithParam("--blocks")
|
||||
}
|
||||
blockType, _ := obj["type"].(string)
|
||||
blockType = strings.TrimSpace(blockType)
|
||||
if blockType == "" {
|
||||
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--blocks[%d].type is required", index).WithParam("--blocks")
|
||||
}
|
||||
body := map[string]interface{}{
|
||||
"name": name,
|
||||
"type": blockType,
|
||||
}
|
||||
if rawConfig, ok := obj["data-config"]; ok {
|
||||
obj["data_config"] = rawConfig
|
||||
}
|
||||
if rawConfig, ok := obj["data_config"]; ok {
|
||||
cfg, err := dashboardBlockDataConfigFromSpec(rawConfig, index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !runtime.Bool("no-validate") {
|
||||
cfg = normalizeDataConfig(cfg)
|
||||
if problems := validateBlockDataConfig(blockType, cfg); len(problems) > 0 {
|
||||
return nil, formatDataConfigErrors(prefixDataConfigProblems(index, problems))
|
||||
}
|
||||
}
|
||||
body["data_config"] = cfg
|
||||
} else if strings.EqualFold(blockType, "text") && !runtime.Bool("no-validate") {
|
||||
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--blocks[%d].data_config.text is required for text blocks", index).WithParam("--blocks")
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func dashboardBlockDataConfigFromSpec(raw interface{}, index int) (map[string]interface{}, error) {
|
||||
switch val := raw.(type) {
|
||||
case map[string]interface{}:
|
||||
return val, nil
|
||||
case string:
|
||||
var cfg map[string]interface{}
|
||||
if err := common.ParseJSON([]byte(strings.TrimSpace(val)), &cfg); err != nil {
|
||||
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--blocks[%d].data_config must be a JSON object or object-encoded string", index).WithParam("--blocks").WithCause(err)
|
||||
}
|
||||
if cfg == nil {
|
||||
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--blocks[%d].data_config must be a JSON object", index).WithParam("--blocks")
|
||||
}
|
||||
return cfg, nil
|
||||
default:
|
||||
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--blocks[%d].data_config must be a JSON object", index).WithParam("--blocks")
|
||||
}
|
||||
}
|
||||
|
||||
func prefixDataConfigProblems(index int, problems []string) []string {
|
||||
prefixed := make([]string, 0, len(problems))
|
||||
for _, problem := range problems {
|
||||
prefixed = append(prefixed, "blocks["+strconv.Itoa(index)+"].data_config: "+problem)
|
||||
}
|
||||
return prefixed
|
||||
}
|
||||
|
||||
func executeDashboardBlockBatchCreate(runtime *common.RuntimeContext) error {
|
||||
specs, err := parseDashboardBlockCreateSpecs(runtime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
params := map[string]interface{}{}
|
||||
if userIDType := strings.TrimSpace(runtime.Str("user-id-type")); userIDType != "" {
|
||||
params["user_id_type"] = userIDType
|
||||
}
|
||||
created := make([]interface{}, 0, len(specs))
|
||||
for _, spec := range specs {
|
||||
data, err := baseV3Call(runtime, "POST", baseV3Path("bases", runtime.Str("base-token"), "dashboards", runtime.Str("dashboard-id"), "blocks"), params, spec.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
created = append(created, compactCreatedDashboardBlock(spec.Index, data))
|
||||
}
|
||||
runtime.Out(map[string]interface{}{
|
||||
"created": true,
|
||||
"created_count": len(created),
|
||||
"dashboard_id": runtime.Str("dashboard-id"),
|
||||
"blocks": created,
|
||||
}, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
func compactCreatedDashboardBlock(index int, data map[string]interface{}) map[string]interface{} {
|
||||
item := map[string]interface{}{"index": index}
|
||||
for _, key := range []string{"block_id", "id", "name", "type"} {
|
||||
if value, ok := data[key]; ok {
|
||||
item[key] = value
|
||||
}
|
||||
}
|
||||
return item
|
||||
}
|
||||
@@ -93,6 +93,7 @@ func Shortcuts() []common.Shortcut {
|
||||
BaseDashboardBlockGet,
|
||||
BaseDashboardBlockGetData,
|
||||
BaseDashboardBlockCreate,
|
||||
BaseDashboardBlockBatchCreate,
|
||||
BaseDashboardBlockUpdate,
|
||||
BaseDashboardBlockDelete,
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ metadata:
|
||||
| 表单提交 | `+form-submit` | 先读 [lark-base-form-detail.md](references/lark-base-form-detail.md) 获取题目、filter 和附件所需 `base_token`;提交 JSON 读 [lark-base-form-submit.md](references/lark-base-form-submit.md) |
|
||||
| 表单题目创建/更新 | `+form-questions-create` / `+form-questions-update` | 读 [lark-base-form-questions-create.md](references/lark-base-form-questions-create.md) / [lark-base-form-questions-update.md](references/lark-base-form-questions-update.md) |
|
||||
| 其他表单管理 | `+form-list/get/detail/create/update/delete` / `+form-questions-list/delete` | `+form-detail` 读 [lark-base-form-detail.md](references/lark-base-form-detail.md);删除前确认目标表单 |
|
||||
| 仪表盘与组件 | `+dashboard-*` / `+dashboard-block-*` | 提到图表/看板/block 时先读 [lark-base-dashboard.md](references/lark-base-dashboard.md);组件 `data_config` 读 [dashboard-block-data-config.md](references/dashboard-block-data-config.md);读取图表计算结果用 `+dashboard-block-get-data` |
|
||||
| 仪表盘与组件 | `+dashboard-*` / `+dashboard-block-*` | 提到图表/看板/block 时先读 [lark-base-dashboard.md](references/lark-base-dashboard.md);新建 2 个及以上组件优先用 `+dashboard-block-batch-create`;组件 `data_config` 读 [dashboard-block-data-config.md](references/dashboard-block-data-config.md);读取图表计算结果用 `+dashboard-block-get-data` |
|
||||
| Workflow | `+workflow-*` | 创建/更新或理解 steps 时读入口 [lark-base-workflow-guide.md](references/lark-base-workflow-guide.md) 和 steps JSON SSOT [lark-base-workflow-schema.md](references/lark-base-workflow-schema.md);list/get/enable/disable 只处理 workflow ID 与启停状态 |
|
||||
| 高级权限与角色 | `+advperm-*` / `+role-*` | 角色操作先读入口 [lark-base-role-guide.md](references/lark-base-role-guide.md);角色 create/update 或解读完整配置再读权限 JSON SSOT [role-config.md](references/role-config.md);系统角色不可删除;关闭高级权限会影响自定义角色 |
|
||||
|
||||
@@ -122,7 +122,7 @@ metadata:
|
||||
|
||||
## Dashboard / Workflow / Role
|
||||
|
||||
- Dashboard 的复杂点是 block 的 `data_config`,不是 list/get/create/delete 命令参数。创建或更新 block 前先读 [dashboard-block-data-config.md](references/dashboard-block-data-config.md),组件必须串行创建;`+dashboard-arrange` 是服务端智能布局,只在用户明确要求重排/美化时执行。`+dashboard-block-get-data` 读取图表最终计算结果,不返回 block 名称、类型、布局或 `data_config`;需要元数据先用 `+dashboard-block-get`。
|
||||
- Dashboard 的复杂点是 block 的 `data_config`,不是 list/get/create/delete 命令参数。创建或更新 block 前先读 [dashboard-block-data-config.md](references/dashboard-block-data-config.md)。需要新建 2 个及以上组件时优先用 `+dashboard-block-batch-create --blocks '[...]'`,由 CLI 内部串行创建并返回紧凑摘要;只建 1 个组件、调试单个配置或修复某个失败组件时再用 `+dashboard-block-create`。`+dashboard-arrange` 是服务端智能布局,只在用户明确要求重排/美化时执行。`+dashboard-block-get-data` 读取图表最终计算结果,不返回 block 名称、类型、布局或 `data_config`;需要元数据先用 `+dashboard-block-get`。
|
||||
- Workflow 的复杂点是 `steps` 结构。创建、更新或解释完整 workflow 时读入口 [lark-base-workflow-guide.md](references/lark-base-workflow-guide.md) 和 steps JSON SSOT [lark-base-workflow-schema.md](references/lark-base-workflow-schema.md);enable/disable/list 只需确认 workflow ID、当前启停状态和用户意图。
|
||||
- Role 的复杂点是权限 JSON。角色操作先读入口 [lark-base-role-guide.md](references/lark-base-role-guide.md);`+role-create` 只支持自定义角色;`+role-update` 是 delta merge;角色 create/update 或解读完整配置时读权限 JSON SSOT [role-config.md](references/role-config.md)。`+role-delete` 只适用于自定义角色,系统角色不可删除;删除角色和关闭高级权限前必须确认目标和影响。
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@ user / created_by / updated_by: is, isNot, isEmpty, isNotEmpty
|
||||
|
||||
## data_config 通用结构
|
||||
|
||||
创建多个组件时,把每个组件写成 `{"name":"...","type":"...","data_config":{...}}`,整体放入 `+dashboard-block-batch-create --blocks '[...]'`。只创建或修复单个组件时,才把本页模板直接传给 `+dashboard-block-create --data-config '{...}'`。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `table_name` | string | 关联数据表名称 |
|
||||
|
||||
@@ -15,7 +15,7 @@ Dashboard 是 Base 中的数据可视化看板,可以把表格数据变成**
|
||||
| 你想做什么 | 用这些命令 | 关键文档 |
|
||||
|------|-----------|---------|
|
||||
| 创建/删除/改名称 | `+dashboard-create/delete/update` | 本页下方「仪表盘管理」 |
|
||||
| 在仪表盘里添加组件 | `+dashboard-block-create` | 先定位 dashboard、表和字段,再读 [dashboard-block-data-config.md](dashboard-block-data-config.md) 构造 `data_config` |
|
||||
| 在仪表盘里添加组件 | `+dashboard-block-batch-create` / `+dashboard-block-create` | 新建 2 个及以上组件优先用 batch;只建 1 个组件或修复单个组件时用 create。先定位 dashboard、表和字段,再读 [dashboard-block-data-config.md](dashboard-block-data-config.md) 构造 `data_config` |
|
||||
| 修改组件 | `+dashboard-block-update` | 先读 block 现状,再读 [dashboard-block-data-config.md](dashboard-block-data-config.md) 决定替换哪些顶层 key |
|
||||
| 查看仪表盘有哪些组件 | `+dashboard-get` 或 `+dashboard-block-list` | 本页下方「查看仪表盘」 |
|
||||
| 读取图表计算结果 | `+dashboard-block-get-data` | 返回图表最终数据协议;需要 block 元数据先用 `+dashboard-block-get` |
|
||||
@@ -39,27 +39,18 @@ lark-cli base +field-list --base-token xxx --table-id <table_id>
|
||||
# 第 3 步:规划应该创建哪些组件(根据用户需求确定组件类型和数量)
|
||||
# 例如:总销售额(指标卡)、月度趋势(折线图)、品类占比(饼图)
|
||||
|
||||
# 第 4 步:顺序创建每个组件(必须串行执行,不能并发)
|
||||
# 第 4 步:批量创建多个组件(CLI 内部会串行执行,不能并发)
|
||||
# 重要:创建组件前,先确定 dashboard_id、组件 name/type 和真实表字段
|
||||
# 再阅读 dashboard-block-data-config.md 了解 data_config 结构、组件类型和 filter 规则
|
||||
|
||||
# 第 1 个组件
|
||||
lark-cli base +dashboard-block-create \
|
||||
# 创建 2 个及以上组件时优先用 batch,避免多轮工具调用和重复返回
|
||||
lark-cli base +dashboard-block-batch-create \
|
||||
--base-token xxx \
|
||||
--dashboard-id blk_xxx \
|
||||
--name "总销售额" \
|
||||
--type statistics \
|
||||
--data-config '{"table_name":"订单表","series":[{"field_name":"金额","rollup":"SUM"}]}'
|
||||
|
||||
# 第 2 个组件(等上一个完成后再执行)
|
||||
lark-cli base +dashboard-block-create \
|
||||
--base-token xxx \
|
||||
--dashboard-id blk_xxx \
|
||||
--name "月度趋势" \
|
||||
--type line \
|
||||
--data-config '{"table_name":"订单表","series":[{"field_name":"金额","rollup":"SUM"}],"group_by":[{"field_name":"月份","mode":"integrated"}]}'
|
||||
|
||||
# 继续创建其他组件...
|
||||
--blocks '[
|
||||
{"name":"总销售额","type":"statistics","data_config":{"table_name":"订单表","series":[{"field_name":"金额","rollup":"SUM"}]}},
|
||||
{"name":"月度趋势","type":"line","data_config":{"table_name":"订单表","series":[{"field_name":"金额","rollup":"SUM"}],"group_by":[{"field_name":"月份","mode":"integrated","sort":{"type":"group","order":"asc"}}]}}
|
||||
]'
|
||||
|
||||
# 第 5 步:组件创建完成后,使用 arrange 命令智能重排布局(可选但推荐)
|
||||
# 默认布局可能不够美观,arrange 会根据组件数量和类型自动优化布局
|
||||
@@ -83,9 +74,17 @@ lark-cli base +dashboard-get --base-token xxx --dashboard-id blk_xxx
|
||||
lark-cli base +table-list --base-token xxx
|
||||
lark-cli base +field-list --base-token xxx --table-id <table_id>
|
||||
|
||||
# 第 4 步:顺序创建每个新组件(必须串行执行,不能并发)
|
||||
# 第 4 步:创建新组件
|
||||
# 重要:先确定 dashboard_id、组件 name/type 和真实表字段
|
||||
# 再阅读 dashboard-block-data-config.md 了解 data_config 结构
|
||||
|
||||
# 新建 2 个及以上组件时优先用 batch
|
||||
lark-cli base +dashboard-block-batch-create \
|
||||
--base-token xxx \
|
||||
--dashboard-id blk_xxx \
|
||||
--blocks '[{"name":"新组件名","type":"column","data_config":{...}}]'
|
||||
|
||||
# 只新建 1 个组件时可用单组件命令
|
||||
lark-cli base +dashboard-block-create \
|
||||
--base-token xxx \
|
||||
--dashboard-id blk_xxx \
|
||||
@@ -207,7 +206,7 @@ A: 常见原因:
|
||||
- 组件创建并发执行(必须串行,等上一个完成再执行下一个)
|
||||
|
||||
**Q: 可以一次创建多个组件吗?**
|
||||
A: 不可以,必须串行执行。等上一个 `+dashboard-block-create` 完成后再执行下一个。
|
||||
A: 可以。使用 `+dashboard-block-batch-create --blocks '[...]'`,CLI 会在一个命令内按顺序串行创建多个组件,并返回每个新组件的 `block_id/name/type` 摘要。不要并发调用多个 `+dashboard-block-create` 写同一个 dashboard;只创建 1 个组件或修复单个失败组件时再用 `+dashboard-block-create`。
|
||||
|
||||
**Q: 组件的 `type` 创建后能改吗?**
|
||||
A: 不能。`+dashboard-block-update` 只能修改 `name` 和 `data_config`,不能修改 `type`。
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package base
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
clie2e "github.com/larksuite/cli/tests/cli_e2e"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestBaseDashboardBlockBatchCreateDryRun(t *testing.T) {
|
||||
setBaseDryRunConfigEnv(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
blocks := `[{"name":"订单数","type":"statistics","data_config":{"table_name":"订单表","count_all":true}},{"name":"月度趋势","type":"line","data_config":{"table_name":"订单表","series":[{"field_name":"金额","rollup":"SUM"}],"group_by":[{"field_name":"月份","mode":"integrated","sort":{"type":"group","order":"asc"}}]}}]`
|
||||
result, err := clie2e.RunCmd(ctx, clie2e.Request{
|
||||
Args: []string{
|
||||
"base", "+dashboard-block-batch-create",
|
||||
"--base-token", "app_x",
|
||||
"--dashboard-id", "dsh_1",
|
||||
"--blocks", blocks,
|
||||
"--user-id-type", "open_id",
|
||||
"--dry-run",
|
||||
},
|
||||
BinaryPath: "../../../lark-cli",
|
||||
DefaultAs: "bot",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
result.AssertExitCode(t, 0)
|
||||
|
||||
output := strings.TrimSpace(result.Stdout)
|
||||
assert.Contains(t, output, "/open-apis/base/v3/bases/app_x/dashboards/dsh_1/blocks")
|
||||
assert.Contains(t, output, `"method": "POST"`)
|
||||
assert.Contains(t, output, `"base_token": "app_x"`)
|
||||
assert.Contains(t, output, `"dashboard_id": "dsh_1"`)
|
||||
assert.Contains(t, output, `"user_id_type": "open_id"`)
|
||||
assert.Contains(t, output, `"blocks"`)
|
||||
assert.Contains(t, output, `"sequential": true`)
|
||||
assert.Contains(t, output, `"name": "订单数"`)
|
||||
assert.Contains(t, output, `"type": "line"`)
|
||||
}
|
||||
@@ -27,6 +27,7 @@
|
||||
| ✓ | base +base-block-move | shortcut | base_block_dryrun_test.go::TestBaseBlockDryRun/move root,move after | `--base-token`; `--block-id`; optional `--parent-id`; `--after-id`; dry-run only | request shape only |
|
||||
| ✓ | base +base-block-rename | shortcut | base_block_dryrun_test.go::TestBaseBlockDryRun/rename | `--base-token`; `--block-id`; `--name`; dry-run only | request shape only |
|
||||
| ✕ | base +dashboard-arrange | shortcut | | none | dashboard workflows not covered |
|
||||
| ✓ | base +dashboard-block-batch-create | shortcut | base_dashboard_block_batch_create_dryrun_test.go::TestBaseDashboardBlockBatchCreateDryRun | `--base-token`; `--dashboard-id`; `--blocks`; dry-run only | request shape only |
|
||||
| ✕ | base +dashboard-block-create | shortcut | | none | dashboard workflows not covered |
|
||||
| ✕ | base +dashboard-block-delete | shortcut | | none | dashboard workflows not covered |
|
||||
| ✕ | base +dashboard-block-get | shortcut | | none | dashboard workflows not covered |
|
||||
|
||||
Reference in New Issue
Block a user