mirror of
https://github.com/larksuite/cli.git
synced 2026-09-14 18:42:53 +08:00
f28a418019
* feat(base): add --position and statistics number_format to dashboard-block create/update
Add an optional top-level --position flag ({x,y,w,h} JSON, parsed but not
coordinate-validated, passed through as a sibling of name/type/data_config) and
optional statistics data_config.number_format ({formatName,precision}) with
light enum + 0-9 integer validation. Both are backward compatible. Body
assembly is unified in a shared buildDashboardBlockBody helper so DryRun and
Execute stay isomorphic. Adds toIntStrict for strict precision parsing, focused
helper/execute/dry-run tests, an E2E dry-run test, and syncs the lark-base
dashboard + data-config skill references.
Co-authored-by: TRAE CLI <noreply@bytedance.com>
* fix(base): validate number_format on update path and add symmetry tests
The dashboard-block-update command parsed data_config but never ran the
statistics number_format check, so an illegal formatName/precision slipped
through locally while create rejected it — violating the SSOT + backend-design
§4.5 promise of CLI-side interception on BOTH paths. Update has no --type flag
(block type is immutable) and intentionally skips strong type validation, so it
now reuses the shared validateNumberFormat sub-validator that
validateBlockDataConfig delegates to, keeping create/update symmetric without
demanding table_name/series on a number_format-only update.
Also: add tests for the --no-validate bypass on create+update, a combined
update carrying position + number_format + name, and extend the DryRun/Execute
body isomorphism assertion to the update path. Clarify the --position flag Desc
that coordinate bounds are advisory (not validated locally or server-side) and
sync the lark-base SKILL.md routing table for --position / number_format.
Co-authored-by: TRAE CLI <noreply@bytedance.com>
* fix(base): align dashboard block validation paths
Validate dashboard block JSON consistently across dry-run and execute paths, enforce statistics number_format boundaries, and add layout precision workflow coverage and documentation.
* fix(base): resolve dashboard layout doc contradictions and harden isomorphism test
Follow-up to the --position / number_format feature, addressing review findings.
Docs (SSOT contradictions):
- SKILL.md:135 and lark-base-dashboard.md still told agents that dashboard
shortcuts cannot set x/y/w/h and to offer auto-layout instead, which would
have left --position unreachable through the skill. Both statements are now
scoped to +dashboard-arrange, which genuinely cannot take coordinates.
- number_format was documented as supporting sub-field merge on update. That
contradicts the update Tips and lark-base-dashboard.md's own data_config
rule ("每个传入的字段内部是全量替换"). Documented as whole-key replacement
and made the update Tip example carry formatName back.
- Trimmed both reference sections: dropped the duplicated field table, the
restated validation blockquote, the standalone bash example and the 4-column
comparison table; kept the enum table and the two load-bearing gotchas.
Reformatted the example to the file's multi-line JSON style, and generalized
the 场景 3 --position argument to '{...}' like its neighbours.
Tests:
- The isomorphism check called buildDashboardBlockBody twice with the same
arguments, so it could never fail. Replaced with an end-to-end comparison of
the --dry-run preview body against the body captured from Execute; verified
it fails under single-path fault injection.
- The live workflow now updates to values distinct from the create call and
asserts them on read-back, instead of asserting substrings that the created
state already satisfied. Dropped the position read-back assertion: this
iteration does not contract get to echo coordinates.
- Filled in the two missing --no-validate cells (create data-config, update
position).
Cleanup:
- Deleted the inline DryRun closures; both commands now point at the
dryRunDashboardBlock* functions, matching the DryRun: dryRunX convention used
across the package and removing the second body-assembly site.
- Rewrote the update comment that referenced review-round codenames and an
external design doc section to be self-contained.
* fix(base): keep dashboard dry-run previews free of empty identifiers
Wiring the block create/update commands to the shared dryRunDashboardBlock*
functions routed them through dryRunDashboardBase, which Set all three
identifiers unconditionally. A create preview has no block_id yet, so it began
advertising "block_id": "" — an argument that reads as failed to resolve.
Skip empty values in the shared helper rather than special-casing create, which
also clears the same pre-existing noise from the +dashboard-arrange preview.
Pinned with a test asserting a create preview carries base_token and
dashboard_id and no block_id.
* fix(base): require complete --position objects and close the arrange/position gap
Round-2 review follow-up. Three findings, all one-liners in effect, that
compounded into a real failure mode: an agent told to "move this chart to the
right half" could send a partial position, have it accepted, and silently
resize the block to nothing — with no coordinate read-back to diagnose it.
- --position now requires all four of x/y/w/h. The server fills missing
coordinates with zero rather than leaving them alone, so a partial object is
a resize disguised as a move. Only the object's shape is checked; coordinate
VALUES stay unvalidated (out-of-range, negative and overlapping still pass
through) as documented. The check is semantic, so --no-validate skips it
while the JSON parse still runs — the same split the rest of this command
pair already uses. Rejected the alternative of validating ranges too: that
would contradict the documented dws-aligned pass-through contract.
- +dashboard-arrange's Tips now point at --position. The cross-reference was
one-directional: create/update told agents about arrange, but arrange — the
command an agent reaches for first when asked to "fix the layout" — never
mentioned that exact placement had become possible.
- Documented that coordinates are write-only this iteration. The reference doc
offered "replicate an existing dashboard's layout" as a use case while the
PR itself scopes out coordinate read-back, sending agents to look for x/y/w/h
that get/list do not return.
Also from the same review:
- The dry-run builders no longer discard buildDashboardBlockBody's error. It is
unreachable while Validate parses the same flags first, but returning nil
makes the runner fail loudly instead of previewing a body with a field
silently missing.
- Added precision cases that run through the real command. The existing
table-driven ones decode with UseNumber and hit toIntStrict's json.Number
branch, which production never takes — parseJSONObject uses a plain
json.Unmarshal, so precision always arrives as float64.
- coverage.md now says which four commands rest solely on the credential-gated
live test that has not been executed yet.
- Marked the number_format fallback claim as unverified against the backend.
* fix(base): close the position guard's null hole and the contract drift it left behind
Round-3 review follow-up. Two of these were introduced by the previous
follow-up commit, not by the original feature.
- The --position completeness guard only asked whether the key was present,
and a JSON null key IS present. `{"x":6,"y":null,"w":null,"h":null}` sailed
through the very check meant to stop it — the exact scenario the guard's own
comment describes. Each coordinate must now actually decode as a number, so
null, strings, objects and bools are rejected alongside missing keys. This is
still a shape check: out-of-range, negative and fractional values keep
passing through as documented. The package's neighbours (`cfg["text"].(string)`,
`table_name`) already validate required fields with a type assertion; this
was the one place that did not. Mutation-verified: reverting the assertion
turns the explicit-nulls case red.
- coverage.md claimed `+dashboard-block-get` "reads back position" while the
test it cites deliberately stopped asserting coordinates — a line the
previous commit invalidated and did not update. It now says number_format
only. The `+dashboard-block-update` row also claimed dry-run coverage for
number_format that only the unexecuted live test provides.
- dashboard-block-data-config.md still said the update path does no local
validation, which commit bb7d8fbc made false in this same PR. An agent
reading it would not expect exit 2 and might reach for --no-validate, which
now also disables the position guard.
Also from that review:
- --no-validate's flag Desc only mentioned data_config; it silently covers the
--position check too. Said so, in both commands.
- Four places stated unverified backend behaviour as fact — including a claim
that the server zeroes missing coordinates, which was the guard's entire
premise, and a "backend defaults to digital" line 23 lines above a blockquote
saying that very fallback was unverified. All reworded to what is actually
known; the guard's rationale is now stated in terms of the request we send.
- E2E dry-run assertions were whole-output substring matches (`"w": 6` could
match anywhere); switched to clie2e.DryRunGet path assertions like the
sibling suites, which also lets them prove position is a top-level sibling
rather than nested in data_config.
- Documented that formatName is case-sensitive, unlike rollup which is
normalized — same object, two conventions, worth saying out loud.
- The --position canonical rewrite's comment claimed it kept Validate/DryRun/
Execute consistent; they re-parse anyway. Its real job is folding @file input
inline so the two paths cannot read a changed file. Comment now says that.
- Named buildDashboardBlockBody's bool at the call sites; covered all three
branches of the identifier skip, not just block_id.
* fix(base): stop dry-run previews leaking route templates; finish the unverified-claim sweep
Round-4 review follow-up. Both findings trace back to earlier follow-up commits
rather than the original feature, and both are the same failure shape: fixing
the instance instead of the class.
- 68bdaccd made dryRunDashboardBase skip empty identifiers, but Set() doubles as
the substitution source for :param placeholders in the URL. Skipping a
declared-but-empty identifier therefore printed the raw route template —
`.../blocks/:block_id` — while also removing `"block_id": ""`, the one signal
that told the caller their argument was empty. An agent whose `$BLOCK_ID` did
not expand would see a preview that looks like the CLI failed to substitute,
with nothing pointing at the real cause. The condition is now whether the
command declares the flag, which is what the comment claimed all along: create
genuinely has no block-id, and that is the case worth omitting.
Not fixed here: a declared-but-empty required identifier still reaches the
wire as a request to the collection endpoint (`baseV3Path` drops empty
segments). That predates this PR and spans the whole base package — worth its
own change rather than guarding two commands and leaving nine inconsistent.
- The isomorphism test only compared bodies, so a preview could target a
different endpoint than Execute and still pass. It now compares method and URL
as well, and rejects any leftover ":" placeholder — that is the mechanism that
would have caught the above.
- 82f72540's message claimed all four unverified backend statements had been
reworded; five survived, three of them in `--help`, where the --position Desc
said server-side acceptance was unverified two lines above a Tip asserting
overlaps are not server-checked. All five now match the wording already used
in lark-base-dashboard.md, and the PR body Summary no longer contradicts its
own Known limitations.
The rejected-alternative for the first item: guarding empty required identifiers
in Validate would be the root-cause fix, but applying it to the two commands
this PR owns while nine sibling dashboard commands keep the old behaviour trades
one inconsistency for another.
* fix(base): stabilize dashboard block validation inputs
* docs(base): clarify precise dashboard layout workflow
* docs(base): align dashboard live coverage status
* docs(base): soften absolute dashboard layout phrasing in skill
Replace "run exactly once / stop" wording for +dashboard-arrange and
--position with intent-based guidance (prefer whole-dashboard arrange,
generally no need to re-read position) so the skill routes agents away
from per-block churn and useless retries without forbidding legitimate
user-driven follow-up adjustments.
Co-authored-by: TRAE CLI <traecli@bytedance.com>
* docs(base): clarify dashboard layout guidance
* docs(skills): move dashboard layout guidance to reference
* docs(base): verify dashboard number format defaults
---------
Co-authored-by: wanglei.75 <wanglei.75@bytedance.com>
Co-authored-by: TRAE CLI <noreply@bytedance.com>
Co-authored-by: TRAE CLI <traecli@bytedance.com>
471 lines
19 KiB
Go
471 lines
19 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||
// SPDX-License-Identifier: MIT
|
||
|
||
package base
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"strings"
|
||
|
||
"github.com/larksuite/cli/errs"
|
||
"github.com/larksuite/cli/shortcuts/common"
|
||
)
|
||
|
||
// dashboardIDFlag returns a Flag for dashboard ID.
|
||
func dashboardIDFlag(required bool) common.Flag {
|
||
return common.Flag{Name: "dashboard-id", Desc: "dashboard ID", Required: required}
|
||
}
|
||
|
||
// blockIDFlag returns a Flag for dashboard block ID.
|
||
func blockIDFlag(required bool) common.Flag {
|
||
return common.Flag{Name: "block-id", Desc: "dashboard block ID", Required: required}
|
||
}
|
||
|
||
// includeBlockType / omitBlockType name buildDashboardBlockBody's bool at the
|
||
// call sites: create sends the block type, update must not (the API rejects a
|
||
// type change).
|
||
const (
|
||
includeBlockType = true
|
||
omitBlockType = false
|
||
)
|
||
|
||
// buildDashboardBlockBody assembles the request body shared by the dashboard
|
||
// block create and update commands. It is the single source of truth for body
|
||
// shape so DryRun and Execute stay isomorphic across all call sites.
|
||
//
|
||
// - includeType controls whether the block type is emitted (create only).
|
||
// - The optional top-level position object is parsed as JSON and passed
|
||
// through verbatim as a sibling of name/type/data_config; coordinate values
|
||
// are NOT validated (aligns with dws grid semantics).
|
||
// - data-config and position are parsed as JSON objects. Validate performs
|
||
// this same parse before either DryRun or Execute, so malformed input cannot
|
||
// disappear from a preview while failing only on the live request.
|
||
func buildDashboardBlockBody(pc *parseCtx, runtime *common.RuntimeContext, includeType bool) (map[string]interface{}, error) {
|
||
body := map[string]interface{}{}
|
||
if name := strings.TrimSpace(runtime.Str("name")); name != "" {
|
||
body["name"] = name
|
||
}
|
||
if includeType {
|
||
if blockType := strings.TrimSpace(runtime.Str("type")); blockType != "" {
|
||
body["type"] = blockType
|
||
}
|
||
}
|
||
if raw := strings.TrimSpace(runtime.Str("data-config")); raw != "" {
|
||
parsed, err := parseJSONObject(pc, raw, "data-config")
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
body["data_config"] = parsed
|
||
}
|
||
if raw := strings.TrimSpace(runtime.Str("position")); raw != "" {
|
||
parsed, err := parseJSONObject(pc, raw, "position")
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
body["position"] = parsed
|
||
}
|
||
return body, nil
|
||
}
|
||
|
||
// positionKeys are the four grid coordinates a position object must carry.
|
||
// A partial object is rejected because a missing coordinate is not "leave this
|
||
// one alone" — an update meant to move a block sideways would arrive without a
|
||
// size. An explicit null is rejected for the same reason a missing key is:
|
||
// `{"x":6,"y":null}` carries exactly as little information as `{"x":6}`, so
|
||
// presence alone is not enough, the value has to actually be a number.
|
||
var positionKeys = []string{"x", "y", "w", "h"}
|
||
|
||
// isJSONNumber reports whether v came out of the JSON decoder as a number.
|
||
// float64 is what the production path yields (parseJSONObject uses a plain
|
||
// json.Unmarshal); json.Number is accepted so the check keeps working if that
|
||
// decoder ever switches to UseNumber.
|
||
func isJSONNumber(v interface{}) bool {
|
||
switch v.(type) {
|
||
case float64, json.Number:
|
||
return true
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
// validateDashboardBlockPosition parses the optional --position flag as a JSON
|
||
// object to fail fast on malformed input. Coordinate *values* (x/y/w/h) are NOT
|
||
// validated — out-of-range, negative and overlapping coordinates pass through
|
||
// verbatim, aligning with dws grid semantics and leaving overlap handling to
|
||
// the server. Only the object's shape is enforced.
|
||
//
|
||
// The JSON parse always runs, including with --no-validate, because it is
|
||
// required for DryRun/Execute request-shape parity. The key-completeness check
|
||
// is semantic, so --no-validate skips it like the rest of the semantic layer.
|
||
func validateDashboardBlockPosition(pc *parseCtx, runtime *common.RuntimeContext) error {
|
||
raw := strings.TrimSpace(runtime.Str("position"))
|
||
if raw == "" {
|
||
return nil
|
||
}
|
||
pos, err := parseJSONObject(pc, raw, "position")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if !runtime.Bool("no-validate") {
|
||
var missing []string
|
||
for _, key := range positionKeys {
|
||
if v, ok := pos[key]; !ok || !isJSONNumber(v) {
|
||
missing = append(missing, key)
|
||
}
|
||
}
|
||
if len(missing) > 0 {
|
||
return errs.NewValidationError(errs.SubtypeInvalidArgument,
|
||
"--position 的 %s 缺失或不是数字;x/y/w/h 必须同时提供且为数值"+
|
||
"(position 按整体提交,不做逐字段合并,残缺对象无法表达一个完整位置)",
|
||
strings.Join(missing, "/")).WithParam("--position")
|
||
}
|
||
}
|
||
// Fold an @file input into inline JSON so DryRun and Execute do not each
|
||
// re-open the file — the preview and the request are then guaranteed to
|
||
// describe the same bytes even if the file changes underneath us.
|
||
b, _ := json.Marshal(pos)
|
||
_ = runtime.Cmd.Flags().Set("position", string(b))
|
||
return nil
|
||
}
|
||
|
||
// dryRunDashboardBase returns a base DryRunAPI carrying the dashboard
|
||
// identifiers this command actually takes. The test is whether the command
|
||
// declares the flag, not whether the value is non-empty: Set doubles as the
|
||
// substitution source for :param placeholders in the URL, so skipping a
|
||
// declared-but-empty identifier would leave the raw template in the preview and
|
||
// hide the fact that the argument was empty. A create command simply has no
|
||
// block-id flag, and that is the case worth omitting.
|
||
func dryRunDashboardBase(runtime *common.RuntimeContext) *common.DryRunAPI {
|
||
api := common.NewDryRunAPI()
|
||
for key, flag := range map[string]string{
|
||
"base_token": "base-token",
|
||
"dashboard_id": "dashboard-id",
|
||
"block_id": "block-id",
|
||
} {
|
||
if runtime.Cmd.Flags().Lookup(flag) != nil {
|
||
api.Set(key, strings.TrimSpace(runtime.Str(flag)))
|
||
}
|
||
}
|
||
return api
|
||
}
|
||
|
||
// dryRunDashboardList returns a DryRunAPI for listing dashboards.
|
||
func dryRunDashboardList(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||
params := map[string]interface{}{}
|
||
params["page_size"] = runtime.Int("page-size")
|
||
if pageToken := strings.TrimSpace(runtime.Str("page-token")); pageToken != "" {
|
||
params["page_token"] = pageToken
|
||
}
|
||
return dryRunDashboardBase(runtime).
|
||
GET("/open-apis/base/v3/bases/:base_token/dashboards").
|
||
Params(params)
|
||
}
|
||
|
||
// dryRunDashboardGet returns a DryRunAPI for getting a dashboard.
|
||
func dryRunDashboardGet(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||
return dryRunDashboardBase(runtime).
|
||
GET("/open-apis/base/v3/bases/:base_token/dashboards/:dashboard_id")
|
||
}
|
||
|
||
// dryRunDashboardCreate returns a DryRunAPI for creating a dashboard.
|
||
func dryRunDashboardCreate(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||
body := map[string]interface{}{"name": runtime.Str("name")}
|
||
if themeStyle := strings.TrimSpace(runtime.Str("theme-style")); themeStyle != "" {
|
||
body["theme"] = map[string]interface{}{"theme_style": themeStyle}
|
||
}
|
||
return dryRunDashboardBase(runtime).
|
||
POST("/open-apis/base/v3/bases/:base_token/dashboards").
|
||
Body(body)
|
||
}
|
||
|
||
// dryRunDashboardUpdate returns a DryRunAPI for updating a dashboard.
|
||
func dryRunDashboardUpdate(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||
body := map[string]interface{}{}
|
||
if name := strings.TrimSpace(runtime.Str("name")); name != "" {
|
||
body["name"] = name
|
||
}
|
||
if themeStyle := strings.TrimSpace(runtime.Str("theme-style")); themeStyle != "" {
|
||
body["theme"] = map[string]interface{}{"theme_style": themeStyle}
|
||
}
|
||
return dryRunDashboardBase(runtime).
|
||
PATCH("/open-apis/base/v3/bases/:base_token/dashboards/:dashboard_id").
|
||
Body(body)
|
||
}
|
||
|
||
// dryRunDashboardDelete returns a DryRunAPI for deleting a dashboard.
|
||
func dryRunDashboardDelete(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||
return dryRunDashboardBase(runtime).
|
||
DELETE("/open-apis/base/v3/bases/:base_token/dashboards/:dashboard_id")
|
||
}
|
||
|
||
// dryRunDashboardBlockList returns a DryRunAPI for listing dashboard blocks.
|
||
func dryRunDashboardBlockList(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||
params := map[string]interface{}{}
|
||
params["page_size"] = runtime.Int("page-size")
|
||
if pageToken := strings.TrimSpace(runtime.Str("page-token")); pageToken != "" {
|
||
params["page_token"] = pageToken
|
||
}
|
||
return dryRunDashboardBase(runtime).
|
||
GET("/open-apis/base/v3/bases/:base_token/dashboards/:dashboard_id/blocks").
|
||
Params(params)
|
||
}
|
||
|
||
// dryRunDashboardBlockGet returns a DryRunAPI for getting a dashboard block.
|
||
func dryRunDashboardBlockGet(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||
params := map[string]interface{}{}
|
||
if userIDType := strings.TrimSpace(runtime.Str("user-id-type")); userIDType != "" {
|
||
params["user_id_type"] = userIDType
|
||
}
|
||
return dryRunDashboardBase(runtime).
|
||
GET("/open-apis/base/v3/bases/:base_token/dashboards/:dashboard_id/blocks/:block_id").
|
||
Params(params)
|
||
}
|
||
|
||
// dryRunDashboardBlockGetData returns a DryRunAPI for getting computed data for a dashboard block.
|
||
func dryRunDashboardBlockGetData(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||
return common.NewDryRunAPI().
|
||
GET("/open-apis/base/v3/bases/:base_token/dashboards/blocks/:block_id/data").
|
||
Set("base_token", runtime.Str("base-token")).
|
||
Set("block_id", runtime.Str("block-id"))
|
||
}
|
||
|
||
// dryRunDashboardBlockCreate returns a DryRunAPI for creating a dashboard block.
|
||
func dryRunDashboardBlockCreate(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||
pc := newParseCtx(runtime)
|
||
body, err := buildDashboardBlockBody(pc, runtime, includeBlockType)
|
||
if err != nil {
|
||
// Unreachable while Validate parses the same flags first. Returning nil
|
||
// makes the runner fail loudly instead of previewing a body that is
|
||
// silently missing a field the real request would have carried.
|
||
return nil
|
||
}
|
||
|
||
params := map[string]interface{}{}
|
||
if userIDType := strings.TrimSpace(runtime.Str("user-id-type")); userIDType != "" {
|
||
params["user_id_type"] = userIDType
|
||
}
|
||
return dryRunDashboardBase(runtime).
|
||
POST("/open-apis/base/v3/bases/:base_token/dashboards/:dashboard_id/blocks").
|
||
Params(params).
|
||
Body(body)
|
||
}
|
||
|
||
// dryRunDashboardBlockUpdate returns a DryRunAPI for updating a dashboard block.
|
||
func dryRunDashboardBlockUpdate(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||
pc := newParseCtx(runtime)
|
||
body, err := buildDashboardBlockBody(pc, runtime, omitBlockType)
|
||
if err != nil {
|
||
// See dryRunDashboardBlockCreate: fail loudly rather than preview a
|
||
// body that diverges from what Execute would send.
|
||
return nil
|
||
}
|
||
params := map[string]interface{}{}
|
||
if userIDType := strings.TrimSpace(runtime.Str("user-id-type")); userIDType != "" {
|
||
params["user_id_type"] = userIDType
|
||
}
|
||
return dryRunDashboardBase(runtime).
|
||
PATCH("/open-apis/base/v3/bases/:base_token/dashboards/:dashboard_id/blocks/:block_id").
|
||
Params(params).
|
||
Body(body)
|
||
}
|
||
|
||
// dryRunDashboardBlockDelete returns a DryRunAPI for deleting a dashboard block.
|
||
func dryRunDashboardBlockDelete(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||
return dryRunDashboardBase(runtime).
|
||
DELETE("/open-apis/base/v3/bases/:base_token/dashboards/:dashboard_id/blocks/:block_id")
|
||
}
|
||
|
||
// ── Dashboard CRUD ──────────────────────────────────────────────────
|
||
|
||
// executeDashboardList lists all dashboards in a base.
|
||
func executeDashboardList(runtime *common.RuntimeContext) error {
|
||
params := map[string]interface{}{}
|
||
params["page_size"] = runtime.Int("page-size")
|
||
if pageToken := strings.TrimSpace(runtime.Str("page-token")); pageToken != "" {
|
||
params["page_token"] = pageToken
|
||
}
|
||
data, err := baseV3Call(runtime, "GET", baseV3Path("bases", runtime.Str("base-token"), "dashboards"), params, nil)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
runtime.Out(data, nil)
|
||
return nil
|
||
}
|
||
|
||
// executeDashboardGet retrieves a dashboard by ID.
|
||
func executeDashboardGet(runtime *common.RuntimeContext) error {
|
||
data, err := baseV3Call(runtime, "GET", baseV3Path("bases", runtime.Str("base-token"), "dashboards", runtime.Str("dashboard-id")), nil, nil)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
runtime.Out(map[string]interface{}{"dashboard": data}, nil)
|
||
return nil
|
||
}
|
||
|
||
// executeDashboardCreate creates a new dashboard.
|
||
func executeDashboardCreate(runtime *common.RuntimeContext) error {
|
||
body := map[string]interface{}{"name": runtime.Str("name")}
|
||
if themeStyle := strings.TrimSpace(runtime.Str("theme-style")); themeStyle != "" {
|
||
body["theme"] = map[string]interface{}{"theme_style": themeStyle}
|
||
}
|
||
data, err := baseV3Call(runtime, "POST", baseV3Path("bases", runtime.Str("base-token"), "dashboards"), nil, body)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
runtime.Out(map[string]interface{}{"dashboard": data, "created": true}, nil)
|
||
return nil
|
||
}
|
||
|
||
// executeDashboardUpdate updates an existing dashboard.
|
||
func executeDashboardUpdate(runtime *common.RuntimeContext) error {
|
||
body := map[string]interface{}{}
|
||
if name := strings.TrimSpace(runtime.Str("name")); name != "" {
|
||
body["name"] = name
|
||
}
|
||
if themeStyle := strings.TrimSpace(runtime.Str("theme-style")); themeStyle != "" {
|
||
body["theme"] = map[string]interface{}{"theme_style": themeStyle}
|
||
}
|
||
data, err := baseV3Call(runtime, "PATCH", baseV3Path("bases", runtime.Str("base-token"), "dashboards", runtime.Str("dashboard-id")), nil, body)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
runtime.Out(map[string]interface{}{"dashboard": data, "updated": true}, nil)
|
||
return nil
|
||
}
|
||
|
||
// executeDashboardDelete deletes a dashboard by ID.
|
||
func executeDashboardDelete(runtime *common.RuntimeContext) error {
|
||
_, err := baseV3Call(runtime, "DELETE", baseV3Path("bases", runtime.Str("base-token"), "dashboards", runtime.Str("dashboard-id")), nil, nil)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
runtime.Out(map[string]interface{}{"deleted": true, "dashboard_id": runtime.Str("dashboard-id")}, nil)
|
||
return nil
|
||
}
|
||
|
||
// ── Dashboard Block CRUD ────────────────────────────────────────────
|
||
|
||
// executeDashboardBlockList lists all blocks in a dashboard.
|
||
func executeDashboardBlockList(runtime *common.RuntimeContext) error {
|
||
params := map[string]interface{}{}
|
||
params["page_size"] = runtime.Int("page-size")
|
||
if pageToken := strings.TrimSpace(runtime.Str("page-token")); pageToken != "" {
|
||
params["page_token"] = pageToken
|
||
}
|
||
data, err := baseV3Call(runtime, "GET", baseV3Path("bases", runtime.Str("base-token"), "dashboards", runtime.Str("dashboard-id"), "blocks"), params, nil)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
runtime.Out(data, nil)
|
||
return nil
|
||
}
|
||
|
||
// executeDashboardBlockGet retrieves a dashboard block by ID.
|
||
func executeDashboardBlockGet(runtime *common.RuntimeContext) error {
|
||
params := map[string]interface{}{}
|
||
if userIDType := strings.TrimSpace(runtime.Str("user-id-type")); userIDType != "" {
|
||
params["user_id_type"] = userIDType
|
||
}
|
||
data, err := baseV3Call(runtime, "GET", baseV3Path("bases", runtime.Str("base-token"), "dashboards", runtime.Str("dashboard-id"), "blocks", runtime.Str("block-id")), params, nil)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
runtime.Out(map[string]interface{}{"block": data}, nil)
|
||
return nil
|
||
}
|
||
|
||
// executeDashboardBlockGetData retrieves computed data for a dashboard chart block.
|
||
func executeDashboardBlockGetData(runtime *common.RuntimeContext) error {
|
||
data, err := baseV3Call(runtime, "GET", baseV3Path("bases", runtime.Str("base-token"), "dashboards", "blocks", runtime.Str("block-id"), "data"), nil, nil)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
runtime.Out(data, nil)
|
||
return nil
|
||
}
|
||
|
||
// executeDashboardBlockCreate creates a new dashboard block.
|
||
func executeDashboardBlockCreate(runtime *common.RuntimeContext) error {
|
||
pc := newParseCtx(runtime)
|
||
body, err := buildDashboardBlockBody(pc, runtime, includeBlockType)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
params := map[string]interface{}{}
|
||
if userIDType := strings.TrimSpace(runtime.Str("user-id-type")); userIDType != "" {
|
||
params["user_id_type"] = userIDType
|
||
}
|
||
|
||
data, err := baseV3Call(runtime, "POST", baseV3Path("bases", runtime.Str("base-token"), "dashboards", runtime.Str("dashboard-id"), "blocks"), params, body)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
runtime.Out(map[string]interface{}{"block": data, "created": true}, nil)
|
||
return nil
|
||
}
|
||
|
||
// executeDashboardBlockUpdate updates an existing dashboard block.
|
||
func executeDashboardBlockUpdate(runtime *common.RuntimeContext) error {
|
||
pc := newParseCtx(runtime)
|
||
body, err := buildDashboardBlockBody(pc, runtime, omitBlockType)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
params := map[string]interface{}{}
|
||
if userIDType := strings.TrimSpace(runtime.Str("user-id-type")); userIDType != "" {
|
||
params["user_id_type"] = userIDType
|
||
}
|
||
|
||
data, err := baseV3Call(runtime, "PATCH", baseV3Path("bases", runtime.Str("base-token"), "dashboards", runtime.Str("dashboard-id"), "blocks", runtime.Str("block-id")), params, body)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
runtime.Out(map[string]interface{}{"block": data, "updated": true}, nil)
|
||
return nil
|
||
}
|
||
|
||
// executeDashboardBlockDelete deletes a dashboard block by ID.
|
||
func executeDashboardBlockDelete(runtime *common.RuntimeContext) error {
|
||
_, err := baseV3Call(runtime, "DELETE", baseV3Path("bases", runtime.Str("base-token"), "dashboards", runtime.Str("dashboard-id"), "blocks", runtime.Str("block-id")), nil, nil)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
runtime.Out(map[string]interface{}{"deleted": true, "block_id": runtime.Str("block-id")}, nil)
|
||
return nil
|
||
}
|
||
|
||
// ── Dashboard Arrange ────────────────────────────────────────────────
|
||
|
||
// dryRunDashboardArrange returns a DryRunAPI for the dashboard arrange endpoint.
|
||
func dryRunDashboardArrange(_ context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||
params := map[string]interface{}{}
|
||
if userIDType := strings.TrimSpace(runtime.Str("user-id-type")); userIDType != "" {
|
||
params["user_id_type"] = userIDType
|
||
}
|
||
return dryRunDashboardBase(runtime).
|
||
POST("/open-apis/base/v3/bases/:base_token/dashboards/:dashboard_id/arrange").
|
||
Params(params).
|
||
Body(map[string]interface{}{})
|
||
}
|
||
|
||
// executeDashboardArrange sends a POST request to auto-arrange dashboard blocks layout.
|
||
func executeDashboardArrange(runtime *common.RuntimeContext) error {
|
||
params := map[string]interface{}{}
|
||
if userIDType := strings.TrimSpace(runtime.Str("user-id-type")); userIDType != "" {
|
||
params["user_id_type"] = userIDType
|
||
}
|
||
// 请求体为空对象,由服务端智能重排
|
||
data, err := baseV3Call(runtime, "POST", baseV3Path("bases", runtime.Str("base-token"), "dashboards", runtime.Str("dashboard-id"), "arrange"), params, map[string]interface{}{})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if data == nil {
|
||
data = map[string]interface{}{}
|
||
}
|
||
data["arranged"] = true
|
||
runtime.Out(data, nil)
|
||
return nil
|
||
}
|