Skip to content

docs: "dashboards as code" guide for programmatic dashboards (CUB-3521) - #11493

Open
mngr wants to merge 2 commits into
masterfrom
gleb/cub-3521-dashboards-as-code
Open

docs: "dashboards as code" guide for programmatic dashboards (CUB-3521)#11493
mngr wants to merge 2 commits into
masterfrom
gleb/cub-3521-dashboards-as-code

Conversation

@mngr

@mngr mngr commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

Dashboards as code guide (CUB-3521)

Documents the idempotent REST upsert API for managing workbooks, dashboards, and reports as code — Phase 1 of programmatic dashboards, shipped in cubedevinc/cubejs-enterprise#13361.

New page: docs-mintlify/docs/explore-analyze/dashboards/dashboards-as-code.mdx, in the Dashboards nav group, with a discovery cross-link from the dashboards index.

What the merged API is

Two public REST endpoints, keyed by a portable identifier you choose (so a CI/CD pipeline can re-apply the same definitions across deployments without tracking per-environment numeric ids):

  • PUT /deployments/{deploymentId}/workbooks/by-slug/{slug} — upsert a workbook (and its dashboard draft in meta.dashboardDraft, merged into existing metadata)
  • PUT /deployments/{deploymentId}/reports/by-public-id/{publicId} — upsert a report keyed by its account-unique, write-once publicId

Supporting changes in the same PR: POST /reports accepts a client-supplied publicId; PUT /reports/{reportId} accepts a write-once publicId (so a UI-authored report can be adopted into as-code management).

Covers

  • How the pieces fit — report (publicId) / workbook (slug) / dashboard (meta.dashboardDraft, made visible by publishing)
  • The apply flow: author in the UI → export via GET → upsert each report → upsert the workbook → publish
  • Idempotency and the three 409 conflict cases distinguished by code (upsert_branch_changed = retry; cross-deployment = permanent; ambiguous_legacy_id = permanent)
  • publicId rules: 12-char [0-9A-Za-z], write-once, client-suppliable; reserved placeholder shape rejected with 400
  • Legacy reports with synthesized ids and how to adopt a real publicId
  • Cross-links to the auto-generated endpoint reference pages under /api-reference/{reports,workbooks}/…

What changed from the first draft

The original draft documented a runtime-owned YAML-in-project model (dashboards as cube.dev/dashboard/v1 YAML in a dashboards/ directory, read at build time, rendered read-only behind useRuntimeDashboards). That feature did not ship in #13361 — it's a different, unmerged design. This revision replaces that page wholesale with documentation of the REST upsert API that actually merged, and drops the "preview / do not merge" gating since these endpoints are shipped public API.

Accuracy

Every fact checked against the merged source and the public OpenAPI spec (open-api-spec-public-v3.1.yaml) — endpoint paths, request bodies (CreateReportInput / CreateWorkbookInput), the 409 code values, publicId validation, and that dashboard-draft widgets reference reports by per-deployment numeric id (hence the export-then-rewrite step). The endpoint reference pages already exist in api-reference/api.yaml on master; all internal links resolve.

Linear: CUB-3521

@mintlify

mintlify Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
cubed3 🟢 Ready View Preview Aug 6, 2026, 4:13 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

…t API (CUB-3521)

The first draft documented a runtime-owned YAML-in-project model that never
shipped. Rewrite it to reflect what actually merged in cubejs-enterprise#13361:
the idempotent public REST upserts

  PUT /deployments/{id}/workbooks/by-slug/{slug}
  PUT /deployments/{id}/reports/by-public-id/{publicId}

keyed by portable slug / publicId. Covers the author→export→upsert→publish
apply flow, idempotency and the three 409 conflict codes, write-once publicId
adoption (incl. legacy synthesized ids), and cross-links to the generated
endpoint reference pages. Fix the dashboards index cross-link accordingly.
@claude

claude Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Claude finished @mngr's task in 3m 33s —— View job


Docs review — 1 blocking, 3 medium, 2 low. Accuracy against the OpenAPI spec is otherwise excellent.

Full review
  • Gather context (read changed files)
  • Check nav registration + link resolution
  • Verify claims against api-reference/api.yaml
  • Verify docs conventions (docs-mintlify/CLAUDE.md)
  • Post review

What's good

The technical accuracy here is unusually high for a docs PR. I checked every substantive claim against docs-mintlify/api-reference/api.yaml and the following all hold:

  • The two upsert paths, both registered in the Reports / Workbooks nav groups (docs.json:865, docs.json:878).
  • The three 409 cases and their code values — verbatim consistent with the x-mint prose at api.yaml:1377-1404, including that the cross-deployment case carries no code.
  • publicId rules: 12-char [0-9A-Za-z] (api.yaml:4930-4936minLength/maxLength 12, pattern matches), write-once, placeholder shape rejected with 400.
  • meta is merged on workbook upsert, slug normalized (api.yaml:1774-1778).
  • The example publicId revqZ1x8Kp0a is exactly 12 valid chars — nice attention to detail; readers do copy these.
  • The report-body fields named in step 2 (sqlQuery, jsonQuery, pivotItems, meta) all exist on CreateReportInput.
  • Page is registered in docs.json under the Dashboards group, and the index cross-link resolves.
  • The workbook-slug ↔ dashboard-slug equivalence claim checks out — slug lives on the workbook DTO (api.yaml:9027), so keying by workbook slug is the same namespace links: [{ dashboard: … }] targets.

The framing decision — document the REST primitives with an explicit <Note> that Cube doesn't ship a bundle apply command — is the right call, and the export-then-rewrite step for per-deployment numeric ids is the detail most people would have missed.

Findings

# Severity Issue
1 High Both curl examples omit the Bearer prefix → copy-paste gets 401 (inline)
2 Medium Publish step needs a body — PublishDashboardInput has required: [workbookId]; the "idempotent" claim is also unsourced (inline)
3 Medium Apply order: reports carry workbookId, but at step 2 on a fresh deployment the workbook doesn't exist yet (inline)
4 Medium "deployment API key" isn't a thing — it's an account key, optionally deployment-scoped; plus base-URL placeholder and "public endpoints" wording (inline)
5 Low "Cube Cloud REST API" — legacy naming per docs-mintlify/CLAUDE.md (inline)
6 Low Spot-check the …-by-publicid generated slug on the preview — the only link whose summary contains camelCase (inline)

Only #1 is genuinely blocking; the rest are worth a pass before merge.

One editorial note

At 200 lines this runs long against the "be concise, don't pad" guidance in docs-mintlify/CLAUDE.md. The content earns most of its length, but two trims are easy:

  • The Reference section (lines 181-187) re-lists links already inline in the body — the one genuinely additive entry is "Building dashboards in the UI".
  • Reports created before stable ids (lines 171-179) largely restates the ambiguous_legacy_id table row plus the x-mint prose that already appears on the endpoint page. Two sentences plus a link would carry it.

Neither blocks merge.

· branch [`gleb/cub-3521-dashboards-as-code`](https://github.com/cube-js/cube/tree/gleb/cub-3521-dashboards-as-code)

Comment on lines +93 to +99
```bash
curl -X PUT \
"$CUBE_API_URL/api/v1/deployments/$DEPLOYMENT_ID/reports/by-public-id/revqZ1x8Kp0a" \
-H "Authorization: $CUBE_API_TOKEN" \
-H "Content-Type: application/json" \
-d @report-revenue-by-month.json
```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The curl examples will 401 — the Bearer prefix is missing.

api-reference/authentication.mdx:11-18 is explicit: /api/v1/… requests take the token with the Bearer prefix (Api-Key is only accepted on the SCIM endpoint). Both curl blocks on this page send the bare token, so a reader copy-pasting either one gets 401 Unauthorized.

Suggested change
```bash
curl -X PUT \
"$CUBE_API_URL/api/v1/deployments/$DEPLOYMENT_ID/reports/by-public-id/revqZ1x8Kp0a" \
-H "Authorization: $CUBE_API_TOKEN" \
-H "Content-Type: application/json" \
-d @report-revenue-by-month.json
```
```bash
curl -X PUT \
"$CUBE_API_URL/api/v1/deployments/$DEPLOYMENT_ID/reports/by-public-id/revqZ1x8Kp0a" \
-H "Authorization: Bearer $CUBE_API_TOKEN" \
-H "Content-Type: application/json" \
-d @report-revenue-by-month.json

Same fix needed at line 117.

[Fix this →](https://claude.ai/code?q=On%20branch%20gleb%2Fcub-3521-dashboards-as-code%2C%20in%20docs-mintlify%2Fdocs%2Fexplore-analyze%2Fdashboards%2Fdashboards-as-code.mdx%2C%20both%20curl%20examples%20(around%20lines%2096%20and%20117)%20send%20-H%20%22Authorization%3A%20%24CUBE_API_TOKEN%22%20without%20the%20Bearer%20prefix.%20api-reference%2Fauthentication.mdx%20requires%20%22Authorization%3A%20Bearer%20%3Ctoken%3E%22%20for%20%2Fapi%2Fv1%20endpoints.%20Add%20the%20Bearer%20prefix%20to%20both.&repo=cube-js/cube)

Comment on lines +130 to +135
### 4. Publish

Upserting the workbook writes the dashboard **draft**. Publish it to make it
visible to viewers with [`POST /workbooks/{workbookId}/publish`][ref-publish],
using the workbook id returned in step&nbsp;3. Publishing is itself idempotent per
workbook, so it is safe to run on every apply.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The publish step needs a request body, and the idempotency claim isn't sourced.

PublishDashboardInput in api-reference/api.yaml:7323-7348 has required: [workbookId] — so workbookId must be in the body, not just the path, and config / title / dashboardId / allowEmbed are all accepted there. As written ("using the workbook id returned in step 3") a reader will POST with an empty body and get a validation error.

Two asks:

  1. Show the minimal body (a one-line -d '{"workbookId": <id>}' curl, or at least say the body carries workbookId).
  2. Say what config means at publish time — if omitting it publishes the current draft, that's the key sentence for an as-code pipeline and it's the whole reason step 3 → step 4 works.

Also, "Publishing is itself idempotent per workbook" is the one claim on this page I couldn't corroborate — publishDashboard has no x-mint description in api.yaml, unlike the two upsert operations. Worth confirming against the handler before shipping, since pipelines will run it unconditionally on every apply.

Comment on lines +86 to +91
### 2. Upsert each report

For every report, [upsert it by `publicId`][ref-upsert-report]. If a report with
that `publicId` already exists in the deployment it is updated with the fields you
send (same semantics as [`PUT /reports/{reportId}`][ref-update-report]);
otherwise it is created with that `publicId`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ordering gap: a report belongs to a workbook, but the workbook doesn't exist yet at step 2.

CreateReportInput (api.yaml:4953) carries workbookId — a per-deployment numeric id. On a fresh deployment (the staging → production case this page is written for), step 2 runs before the workbook exists, so there is no workbookId to send, and the reports land unattached.

The page never resolves this. Either the order should be workbook-first (upsert the workbook by slug → get its numeric id → upsert reports with that workbookId → re-upsert the workbook with the rewritten dashboardDraft → publish), or the flow should mention PUT /reports/{reportId}/connect-workbook as the attach step. Worth stating explicitly whether dashboard widgets can reference reports that aren't attached to the workbook at all — if they can, say so and this concern goes away.

Comment on lines +54 to +64
## Authenticating

These are public REST endpoints. Authenticate with a deployment API key exactly
as for the rest of the [REST API][ref-api] — see [Authentication][ref-auth] for
how to create a key and pass it. The examples below assume:

```bash
export CUBE_API_URL="https://<your-cube-cloud-host>"
export CUBE_API_TOKEN="<your-api-key>"
export DEPLOYMENT_ID="<your-deployment-id>"
```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three smaller things in this block:

  1. "deployment API key" — there's no such thing per authentication.mdx:20-26: it's an account API key (or OAuth access token) that may optionally be scoped to deployments. Suggest "Authenticate with a Cube API key generated in your account settings; a key scoped to specific deployments must include the target deployment."
  2. Base URL placeholder — the rest of the API reference uses https://<tenant>.cubecloud.dev. Matching it here saves the reader a lookup.
  3. "These are public REST endpoints" reads as unauthenticated on first pass, and then the next sentence says to authenticate. Since the point is just "not internal/preview API", either drop the sentence or say "part of the public [REST API][ref-api]".

[workbooks][ref-workbooks], their [dashboards][ref-dashboards], and the
[reports][ref-reports] the dashboard widgets render — from source control instead
of only through the UI. You keep each asset's definition in Git and apply it to a
deployment with the [Cube Cloud REST API][ref-api], the same way you might manage

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docs-mintlify/CLAUDE.md lists Cube Cloud as legacy naming that shouldn't appear in new content. Use "Cube REST API" here (and https://<tenant>.cubecloud.dev rather than <your-cube-cloud-host> at line 61 — the hostname itself is a literal, so it's fine, but the prose label isn't).

Suggested change
deployment with the [Cube Cloud REST API][ref-api], the same way you might manage
deployment with the [Cube REST API][ref-api], the same way you might manage

Comment on lines +194 to +200
[ref-upsert-workbook]: /api-reference/workbooks/create-or-update-a-workbook-by-slug
[ref-upsert-report]: /api-reference/reports/create-or-update-a-report-by-publicid
[ref-create-report]: /api-reference/reports/create-a-report
[ref-update-report]: /api-reference/reports/update-a-report
[ref-get-report]: /api-reference/reports/get-report
[ref-get-workbook]: /api-reference/workbooks/get-workbook
[ref-publish]: /api-reference/workbooks/publish-dashboard

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All seven targets correspond to real operations in api-reference/api.yaml and the slugs match the summary: values (Create or update a workbook by slug, Get report, Publish dashboard, …), so these should resolve.

One to spot-check on the Mintlify preview before merging: create-or-update-a-report-by-publicid. Every other slug here comes from an all-lowercase summary, but this one slugifies publicId — depending on whether Mintlify splits camelCase, the real page could be …-by-public-id. Worth one click on the preview link since it's the page's most important reference.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant