msyavuz opened a new issue, #44989:
URL: https://github.com/apache/superset/issues/44989
## [SIP] Proposal for Canvas, the document that places widgets
Canvas is the next generation of Superset dashboards: a tree of placements
of SIP-231 widget instances, edited live through small, validated operations by
people and agents alike. It is the "companion Canvas SIP" that SIP-231 (#44875)
defers to for the Canvas document, placements and their filter scope, promoting
and inlining instances, the snapshot export, the listener bus (filter state,
URL serialization, cascading filters) and Canvas permalinks.
- **Agents edit a canvas reliably and cheaply.** "Put the revenue trend next
to the KPIs and make the region filter apply to the whole page." The agent
reads a compact document with readable ids and sends a few named operations;
the server validates every one.
- **A canvas can't be left broken.** Every write is checked against the
tree, each widget's nesting and layout rules, and its props schema. A bad write
is rejected with a JSON-pointer path and changes nothing.
- **Many editors at once.** Edits apply against a base revision; edits that
don't overlap merge, overlapping ones are rejected with the placements in
conflict. Open pages update live.
- **Any widget, from core or an extension.** Containers (tabs, groups, a
filter bar) are widgets too; the canvas grammar stays small and fixed.
- **Filters that follow the layout.** A filter drives the widgets in its tab
by default; a filter in the filter bar drives the whole canvas. Overrides are
explicit and per placement.
- **Upgrades that never lose work.** Placements whose widget is missing or
newer than the server stay as placeholders, exactly as stored.
Existing dashboards keep working alongside Canvas. Conversion and retirement
are phased and gated on milestones.
**Scope.** This SIP covers everything *between* placements: the document,
layout and nesting, filter scope and the listener bus, editing and concurrency,
snapshots, permalinks, storage and access for canvases. Everything *inside* a
placement (what props mean, their schema and validation, queries, data,
renderers and widget versions) is SIP-231's, and this SIP only references it;
the ops that write inline props are this SIP's.
> **Open questions for reviewers** (details at the end)
> 1. Changes this SIP needs from SIP-231: runtime choices for
customizations, and one shared flag.
> 2. Unfinished instances: can a placement hold a widget that isn't fully
configured yet?
> 3. Previewing a change before every viewer sees it.
> 4. Live layout edits: placing a widget can push others down for every
viewer at once. Is that acceptable without an edit mode?
> 5. The op log lets viewers read changes made before they had access.
> 6. History and undo: the op log could power both, but canvases aren't
versioned yet.
> 7. Filters across datasources: matching by column name, or an explicit
mapping on the scope override.
> 8. Conversion from dashboards: which unconverted features block a
dashboard from converting, and which are only reported.
### Motivation
1. **Dashboards are hard for agents to edit.** A dashboard is
`position_json` plus `json_metadata`, saved whole. Every layout change rewrites
the full tree, including `parents` arrays and wrapper `ROW`/`COLUMN`
components, and the API accepts any JSON. In our evaluation (see Evaluation
below), a small model left 4 of 27 hard-tier dashboards unrenderable without
being told, and every model paid 2.7–3.8× more per edit than on the canvas
schemas.
2. **The server doesn't know the dashboard grammar.** Nesting rules live in
the frontend (`isValidChild.ts`); on save the server only repairs unreachable
components. Ids are client-made and never checked.
3. **Last write wins.** A whole-document `PUT` silently overwrites
concurrent edits, which matters more once agents edit alongside people.
4. **Filter scope is stored as layout paths plus caches.** `rootPath` and
`excluded` with derived `chartsInScope`/`tabsInScope` caches; cross-filters and
customizations each have their own config shape.
5. **SIP-231 needs a home for placements.** Widget instances are persisted
or inline; both need a document that places them and scopes their filters, and
inline instances need a way to change.
#### Evaluation: which schema works best for agents
We evaluated candidate schemas by having Claude models make real edits
through MCP tools against a scored copy of a canvas: 26 tasks (16 single-intent
edits on a 15-placement canvas, 10 harder ones on a 51-placement canvas), 3
samples each, with Haiku 4.5 and Sonnet 5.5 on seven canvas schemas and today's
dashboards (which skip one hard task that has no dashboard equivalent), plus
the factorial screening below: 1,722 runs. A run is **clean** when the
requested end state holds and nothing else changed. Known-correct solutions
score 100% clean on every schema, and no task passes on an unchanged canvas.
| Hard tier, clean % / cost per run | Today's dashboards | Canvas schemas |
| --- | --- | --- |
| Haiku 4.5 | 74% / $0.142 | 93% / $0.053 |
| Sonnet 5.5 | 93% / $0.295 | 97% / $0.078 |
A fractional factorial over six schema dimensions (16 schemas, 240 runs per
level) found no reliability effect from referenced vs inline config,
tree-derived vs explicit scope, sparse vs normalized props, or flat vs nested
structure: every level landed within two points. Two dimensions changed cost by
about a third with no reliability loss: **readable ids** instead of UUIDs, and
**leaving resolved placements and scopes out** of the default read. Both are
adopted below (§3, §9). Failures clustered by task (bulk resizing, vague
references) rather than by schema.
### Proposed Change
#### 1. Concepts
Terms follow SIP-231.
- **Widget:** a kind of thing that can be placed, from core or an extension
(SIP-231 §2). Containers are widgets.
- **Widget instance:** one configured occurrence of a widget, **persisted**
(its own UUID, in SIP-231's `WidgetInstance` table) or **inline** (stored on
its placement as SIP-231's `widget_id`, `schema_version` and `props`, spelled
`widget`, `schemaVersion` and `props` in the document's camelCase).
- **Placement:** an instance's spot on a canvas (SIP-231 §5): its id,
layout, children and filter scope. Scope overrides are stored under
`interactions`, keyed by placement id, so every kind's overrides sit together;
they belong to the placement and go when it goes.
- **Canvas:** a document of placements, interactions and settings, plus row
metadata (title, slug, theme, CSS, certification, editors, viewers).
| Owned by this SIP | Owned by SIP-231 |
| --- | --- |
| The document: placements, tree, layout, interactions, settings | Widgets,
instances and their props |
| Containers' layout and nesting rules, scope roles (fields added to
`WidgetBehavior`) | Props schemas, validation, errors, progressive disclosure |
| Filter scope, the listener bus, cascading, permalinks | Runtime filter
vocabulary and its hooks; applying filters to queries |
| Operations (including edits to inline props), concurrency, live updates |
Edits to persisted instances |
| Canvas storage, slugs, access, embedding | `WidgetInstance` storage and
access; instance and placement data routes |
| Document versions (Alembic) | Props versions (migrators) |
| Snapshot format and access rules | What an instance contributes to a
snapshot |
| Canvas MCP tools | Widget MCP tools |
<details>
<summary><b>2. The canvas document</b></summary>
```json
{
"version": 1,
"root": {"layout": {"columns": 24, "gap": 16, "rowUnit": 40}, "children":
["kpis", "overview"]},
"nodes": {
"kpis": {"widget": "group", "schemaVersion": 1, "props": {}, "layout":
{"colSpan": 24, "rowSpan": 4},
"children": ["revenue", "orders"]},
"revenue": {"instance": "6f1c2b7e-2d4a-4c1e-9a53-0f3b8d2e7a10",
"layout": {"colSpan": 6}},
"orders": {"widget": "metric-tile", "schemaVersion": 1,
"props": {"dataBinding": {"datasetId": 7, "metrics":
["count"]}}, "layout": {"colSpan": 6}},
"overview": {"widget": "tabs", "schemaVersion": 1, "props": {},
"layout": {"colSpan": 24, "rowSpan": 12},
"children": ["emea"]},
"emea": {"widget": "tab", "schemaVersion": 1, "props": {"title":
"EMEA"}, "layout": {},
"children": ["region", "trend"]},
"region": {"widget": "filter.select", "schemaVersion": 1,
"props": {"datasetId": 7, "column": "region"}, "layout":
{"colSpan": 6, "rowSpan": 2}},
"trend": {"widget": "echarts", "schemaVersion": 1, "props": {"…": "…"},
"layout": {"colSpan": 18, "rowSpan": 8}}
},
"interactions": {
"filters": {"region": {"mode": "global", "exclude": ["revenue"]}},
"crossFilters": {},
"customizations": {}
},
"settings": {
"refresh": {"interval": 300, "stagger": 5000, "exempt": ["revenue"]},
"colors": {"scheme": "supersetColors", "labelColors": {"EMEA":
"#1f77b4"}},
"display": {"showTimestamps": true},
"crossFilters": {"enabled": true}
}
}
```
| Part | Rule |
| --- | --- |
| `version` | Document format version, mirrored in
`canvases.definition_version` |
| `root` | Not a placement; always exists, addressed as `"root"`. A grid: 24
columns, 16px gap, 40px rows by default |
| `nodes` | Placements, a flat map keyed by placement id. The tree is stored
only in `children` lists, whose order is reading order |
| Persisted placement | `instance`: the instance's UUID. Props, schema
version and access live with the instance |
| Inline placement | `widget`, `schemaVersion`, `props`: explicitly set
values only, valid against the widget's schema at that version |
| `layout` | Where the placement sits in its parent, validated against the
parent's rules |
| Grid layout | 1-based `col`, `row`, `colSpan`, `rowSpan`. `col` and `row`
are set together or omitted to auto-place in reading order. Spans default to
the widget's default size on `add`, else the full grid width and one row.
Colliding explicit positions are pushed down in reading order |
| `interactions` | Scope overrides per kind, keyed by placement (§5) |
| `settings` | Refresh, shared colors, timestamps, cross-filters on or off |
An instance can be placed more than once, even on one canvas; each placement
has its own layout, scope and filter values (SIP-231 §5, §9).
**Inline by default.** A widget added on a canvas is an inline instance,
whatever the widget: it lives and is shared with the canvas, travels with it,
and has no permissions of its own to manage. It becomes persisted when someone
promotes it (§6), to place it elsewhere, use it outside the canvas, or give it
its own access. Existing persisted instances are placed by reference.
</details>
<details>
<summary><b>3. Placement ids</b></summary>
Placement ids are readable slugs, unique within a canvas: `revenue`,
`revenue-trend`, `revenue-trend-2` (`^[a-z0-9]+(-[a-z0-9]+)*$`, at most 64
characters; `root` and `settings` are reserved). The server derives one from an
inline instance's `title` prop, else the widget's name; callers may choose
their own on `add`, so later operations in the same request can reference it, a
UI can draw the placement before the response, and a retried `add` fails
instead of duplicating.
An inline instance's identity is the canvas UUID plus its placement id
(SIP-231 §5). Persisted instances keep UUIDs, which are global.
Readable ids are an agent-facing decision backed by evidence: across 480
runs, readable ids cut agent cost by about a third against UUIDs with no loss
in reliability (see Evaluation).
</details>
<details>
<summary><b>4. Containers and layout</b></summary>
The canvas ships no layout primitives. Containers are widgets, and widgets
declare how they take part in a canvas in SIP-231's `WidgetBehavior` (agreed
with SIP-231, beside its `emits_filters` and `drill`); widget ids in them are
the registered ids, namespaced for extension widgets
(`extensions.<publisher>.<name>.<id>`):
- `container`, `accepted_children` (SIP-231's "which child widgets they
accept"), `allowed_parents`;
- `grid_columns` (children on a grid, collisions resolved by the server), a
`child_layout_model` (any schema, arranged by the container's renderer), or
neither (children take no layout, such as tab panes);
- `bounds_filter_scope` (default true);
- `filter` (sets runtime filters), `customization` (sets runtime choices,
§5) and `filterable` (receives them).
`WidgetUi` carries `default_size` (applied on `add` when a grid layout
leaves spans unset) and min and max spans. Core registers `tabs` (accepts only
`tab`), `tab` (24-column grid, only in `tabs`), `group` (12-column grid) and
`filter.bar` (24-column grid of `filter.select` that doesn't bound scope);
`tabs`, `tab` and `group` bound scope. Of the other built-ins, `echarts` and
`ag-grid-table` are filterable and cross-filter, `metric-tile` and `balloons`
are filterable, `filter.select` is a filter and filterable (so filters cascade,
§5) and `markdown` is a leaf.
The server resolves auto-placement and push-down and returns resolved
positions, so clients never reimplement placement.
</details>
<details>
<summary><b>5. Interactivity: filter scope and the listener bus</b></summary>
**Scope.** Filter scope belongs to placements (SIP-231 §9). Filters,
cross-filter sources and customizations (such as a dynamic group-by) share one
model, declared by widget behavior (`filter`, SIP-231's `emits_filters`,
`customization`) and received by `filterable` widgets:
- **`auto`** (default): drives every filterable placement under the nearest
container that bounds scope, or the whole canvas, never itself, minus
`exclude`. A filter in a tab drives the tab; a filter in the filter bar drives
the canvas. An unresolved container keeps its filters contained.
- **`global`**: every filterable placement, minus `exclude`.
- **`custom`**: exactly `targets`.
Resolved scopes are computed on read, never stored, and returned as
`filterScopes`, `crossFilterScopes` and `customizationScopes`. Cross-filter
scopes resolve empty while cross-filters are off.
**The listener bus** connects placements on an open canvas:
- **Where it lives.** One bus per open canvas page, holding the current
value each filter, cross-filter source and customization has published, keyed
by placement id and kind. Values are view state and never enter the document.
- **How widgets take part.** Through SIP-231's hooks: `emitFilter()`
publishes for the emitting placement, and `useRuntimeFilters()` returns the
values published by placements whose resolved scope includes this one, in
SIP-231's runtime-filter vocabulary (`{column, operator, value, datasource?}`).
A placement sends what reaches it as `runtime_filters` when it requests data
(SIP-231 §8).
- **Customizations.** A customization, such as the dynamic group-by of
SIP-185 (#35158), publishes a choice rather than a filter, with
`emitRuntimeProps({path: value})`. The receiving widget marks the props a
viewer may change at runtime with `x-runtime`, listing the allowed values (for
a group-by, the dimensions the instance offers); `useRuntimeProps()` returns
the choices reaching its placement, and `useWidgetData()` sends them as
`runtime_props`, validated against those values like a write. A viewer picks
among what the stored instance offers and can't add dimensions, metrics or
columns, so the stored definition still owns the query. These are amendments to
SIP-231 (see "Changes this SIP asks of SIP-231").
- **Which placements a filter reaches.** Scope decides the candidates;
datasource decides applicability. Every filter and cross-filter on the bus
carries its `datasource`, and reaches only in-scope placements that query that
datasource, which the server reports per placement with the resolved scopes.
Applying a filter across datasources, by column name or an explicit mapping, is
a later, additive field on scope overrides.
- **Cascading filters.** A filter widget that is also `filterable` receives
the filters whose scope includes it and narrows its options with them, so a
city filter in the scope of a country filter cascades with no separate
dependency setting. Published values are never rewritten by options, so mutual
scopes can't loop.
- **URLs and permalinks.** The bus state serializes to `{placement id:
{kind: value}}`. `POST /api/v1/canvas/{uuid}/permalink` stores it, with the
canvas revision, in the key-value store and returns a key; `/canvas/p/<key>/`
opens the canvas with that state. Values for placements that no longer exist,
or whose scope no longer reaches a target, are dropped on load.
</details>
<details>
<summary><b>6. Editing: operations</b></summary>
`POST /api/v1/canvas/` can seed a new canvas with a whole definition,
validated in full. After that, every change goes through one route, atomically,
against a base revision:
```
PATCH /api/v1/canvas/{id_or_uuid}/definition
{"base_revision": 7, "ops": [
{"op": "add", "id": "kpis", "widget": "group", "layout": {"colSpan": 24,
"rowSpan": 4}},
{"op": "add", "id": "revenue", "parent": "kpis", "instance": "6f1c2b7e-…",
"layout": {"colSpan": 6}},
{"op": "set_scope", "kind": "filter", "id": "region", "scope": {"mode":
"global"}}
]}
→ 200 {"revision": 8, "ops": [{…, "id": "kpis"}, {…, "id": "revenue"}, {…}],
"filterScopes": {…}, "placements": {…}}
```
| Op | Effect |
| --- | --- |
| `add` | Places a persisted (`instance`) or new inline (`widget`, `props`,
optional `schemaVersion`) instance under `parent` (default `root`) at `index`
(default last); inline props are migrated to and stored at the widget's current
schema version |
| `set_props` | Replaces an inline instance's props, with an optional
`schemaVersion` as on `add` |
| `patch_props` | Applies a JSON Patch (RFC 6902) to an inline instance's
props, so an edit to one nested prop doesn't resend the rest |
| `place` | Changes position and size within the parent (drag, resize) |
| `move` | Reparents or reorders, optionally with a new `layout`;
reparenting without one resets the layout to auto-place |
| `remove` | Removes a placement and its subtree, pruning them from scopes
and refresh exemptions |
| `set_scope` | Sets or clears a placement's scope override for one kind |
| `set_settings` | Replaces one settings section; `null` resets it |
| `promote` | Saves an inline instance as a persisted one: creates a
`WidgetInstance` from its widget, schema version and props, with the caller as
editor, and the placement references it. Needs SIP-231 `can_write` on `Widget` |
| `inline` | Copies a persisted instance's widget, schema version and props
onto its placement, detached from the original. Needs read access to the
instance |
- **Editing inline instances.** Inline props change only through canvas ops:
`add`, `set_props` and `patch_props`. Each write is validated in full on the
instance it writes (schema, datasource access, referenced columns and metrics,
and the queries it would build), and leaves other placements alone.
`schemaVersion` on a write lets props saved at an older version be migrated
rather than mislabelled. `promote` and `inline` touch the placement's `props`.
Persisted instances change through SIP-231's instance API with its own revision
check.
- **Atomic and live.** Ops apply in order to a copy; all land or none do. An
accepted request is the stored canvas; there is no edit mode or Save.
- **Concurrency.** Each op touches `(placement, field group)` pairs: `tree`,
`layout`, `props`, one group per scope kind, one per settings section. An `add`
touches only the new placement. A write based on an older revision merges
unless it overlaps a change logged since; then it gets 409 `{message, revision,
conflicts, stale: false}`. `tree` overlaps every group. A base revision ahead
of the canvas, or older than the retained log (or any base before the first
write after creation), gets 409 with `stale: true`: reload. A merged write
whose op no longer applies, such as an `add` into a parent someone removed,
gets 422.
- **Live updates.** Each write publishes `entity.changed` with
`{entity_type: "canvas", id}` only, and open pages refetch the definition
through the authorized API. Without a realtime transport, pages poll
`…/definition/changes?since=N` every 5 seconds and refetch when it reports a
newer revision; that route also returns the logged ops, grouped by revision,
and answers 409 `stale` when `since` is older than the retained log, upon which
the page reloads.
- **Errors** carry JSON-pointer paths, in the SIP-231 Appendix C spirit: 400
`{message: [{path, message}]}` for a malformed request (paths into the body),
and 422 `{message, errors: [{path, message}], operation?}` when an op can't
apply (`/ops/<index>`) or the result is invalid (paths into the document).
</details>
<details>
<summary><b>7. Validation and evolution</b></summary>
- **Strict where a write touches, tolerant elsewhere.** Tree integrity is
always checked. Widget behavior (nesting, spans, child layout, scope roles) is
enforced on the placements a write touches, and inline props are validated
where a write wrote them. A widget that tightens a rule or its schema never
blocks edits elsewhere on the canvas.
- **Placeholders.** A placement whose instance is missing, whose widget
isn't registered (say, an extension was removed), or whose props were saved by
a newer widget version or fail to migrate stays exactly as stored, with its
children. The rest of the canvas renders and stays editable; re-enabling the
widget restores it.
- **Props versions** follow SIP-231 §12: inline props are migrated in memory
on read by the widget's migrators and stored upgraded on the next write.
- **Document versions.** A change to the canvas document format bumps
`version` and ships an Alembic migration with a downgrade, which also clears
the op log so open pages reload; additive changes ship expand/contract across
two releases. A document at any other version, such as one from a newer server,
is refused with 422 rather than read with the wrong schema. Importing an older
snapshot (§11) first converts it with the same transformations its migrations
apply.
</details>
<details>
<summary><b>8. Storage, permissions and data</b></summary>
- **Tables:** `canvases` (integer id plus UUID, audit columns, `title`,
`description`, `definition`, `definition_version`, `revision`, `slug`, `css`,
`theme_id`, certification and external-management columns), `canvas_editors` /
`canvas_viewers` (subject-based, like dashboards), and `canvas_ops` (the op
log, last 500 revisions per canvas).
- **Slug:** optional, unique across canvases, letters, digits, `_` and `-`,
at most 255 characters, never all digits and never `list`. The canvas page is
`/canvas/<slug>/` when set, `/canvas/<id>/` otherwise.
- **Access:** admins, editors and viewers see a canvas, and
`EXTRA_ACCESS_QUERY_FILTERS["canvases"]` can widen that; embedded guests see
none. Writes need editorship (admins always have it); the creator is the
default editor. Seeing a canvas grants no data access by itself: a viewer also
needs access to each persisted instance placed on it and to its datasource
(SIP-231 §8), and placements they can't read render as no-access placeholders.
Granting placed instances through the canvas would add a row to the role and
capability matrix in `SECURITY.md` and is left for a later proposal.
Permissions are `can_read` and `can_write` on `Canvas`; widget APIs use
SIP-231's `Widget` resource.
- **Data:** SIP-231 §8 defines how inline instances are read, through `POST
/api/v1/canvas/{uuid}/placement/{placement_id}/data`. This SIP adds only the
canvas side: the caller needs read access to the canvas, and the placement must
resolve.
- **Feature flag:** one flag, `CANVAS` (off by default), shared with SIP-231
since neither ships without the other. It gates registration of the canvas and
widget REST APIs and MCP tools, the SPA routes and the menu, which 404 and hide
while it is off. Canvases are kept when it is turned off.
</details>
<details>
<summary><b>9. Agent access</b></summary>
MCP tools wrap the same commands as the REST API:
| Tool | Purpose |
| --- | --- |
| `get_canvas` | The document and its revision. Resolved positions and
scopes only on request (`include_resolved`) |
| `apply_canvas_ops` | Apply operations atomically against a base revision;
returns the new revision and the applied ops with their ids, or the errors |
| SIP-231 §13 tools | Discover widgets and read their schemas progressively;
edit persisted instances |
The default read is lean because resolved positions and scopes cost context
and didn't improve reliability in the evaluation. The REST `GET` and `PATCH`
responses always include them, since the page needs them to render.
</details>
<details>
<summary><b>10. Rendering</b></summary>
The page renders the resolved grid. Widget renderers are SIP-231 renderers,
registered once in its renderer registry by namespaced widget id with the
schema versions they support (SIP-231 §10, §12); core registers the containers
and `markdown`. Data and interactivity reach a renderer through SIP-231's
hooks, which the canvas page backs with the placement's instance and the
listener bus, so a renderer doesn't know whether its instance is inline or
persisted or how often it is placed.
On a canvas, renderers also get canvas context: the placement id, shared
colors, whether to show timestamps, and a refresh key that bumps on each
automatic refresh. Containers get their rendered children (with ids, widget ids
and layouts) or a ready-made grid. Each renderer runs inside an error boundary;
placements that don't resolve, or whose widget has no renderer for its schema
version, render a placeholder.
</details>
<details>
<summary><b>11. Snapshot export</b></summary>
`GET /api/v1/canvas/{uuid}/snapshot` returns one self-contained, immutable
document (SIP-231 §5):
- the canvas metadata (title, description, CSS, theme by UUID) and its
definition, with every persisted placement replaced by an inline copy of the
instance as it is at export time;
- for each widget used, its id, `schema_version` and implementation version
(SIP-231 §12).
Copying an instance inline puts it under whoever can open the snapshot, so
the exporter needs read access to every persisted instance; placements they
can't read are exported as unresolved placeholders without props. Snapshots
hold definitions only, never query results: data is read when the snapshot is
opened, under the reader's own access and row-level security. Snapshots aren't
stored by the server. Importing one through `POST /api/v1/canvas/` creates a
new canvas; widgets that are missing or newer than the server stay placeholders
(§7).
</details>
<details>
<summary><b>12. Embedding, reports and export (M2)</b></summary>
- **Embedding.** A canvas gets an embedded configuration (allowed domains),
as dashboards do. Guest tokens gain a `{"type": "canvas", "id": "<uuid>"}`
resource that grants reading that canvas's definition and the data of its
placements, inline or persisted, with the token's row-level security, and
nothing else; the access check is on the placement, so an instance isn't
readable through a canvas it isn't placed on. This adds a guest-token row to
the role and capability matrix in `SECURITY.md`, as dashboard guest tokens
have; grants through a canvas for regular viewers stay out of scope (§8).
- **Reports and thumbnails.** A canvas screenshot report or thumbnail
captures the layout, so it renders the canvas page headlessly, as dashboard
ones do. Data-only reports on a placement read the placement's data route and
need no browser.
- **Export and import.** A canvas bundle holds the canvas and the persisted
instances it places, by UUID, so importing updates them in place, alongside
today's asset bundles. A snapshot (§11) is the self-contained alternative.
</details>
### New or Changed Public Interfaces
<details>
<summary>Full list</summary>
- **REST** (`/api/v1/canvas/`): CRUD with editors and viewers (create may
seed a definition or import a snapshot); `GET|PATCH /{id_or_uuid}/definition`,
`GET /{id_or_uuid}/definition/changes?since=N`; `GET /schema` (JSON Schemas for
the definition, operations and grid placement, plus every registered widget's
behavior and UI metadata); `POST /{uuid}/placement/{placement_id}/data`; `GET
/{uuid}/snapshot`; `POST /{uuid}/permalink`, opened at `/canvas/p/<key>/`.
- **Operations:** `add`, `set_props`, `patch_props`, `place`, `move`,
`remove`, `set_scope`, `set_settings`, `promote`, `inline`.
- **MCP:** `get_canvas`, `apply_canvas_ops`, alongside SIP-231's §13 tools.
- **`superset-core`:** `superset_core.canvas.GridPlacement` and
`InstanceResolver` (looks up persisted instances); the container and scope
fields this SIP adds to SIP-231's `WidgetBehavior` and `WidgetUi`.
- **`@apache-superset/core`:** canvas context for renderers (placement id,
colors, timestamps, refresh key, children and grid for containers);
`getActiveCanvas` / `onDidChangeActiveCanvas`. Renderers register in SIP-231's
registry, and the canvas page implements SIP-231's `useWidgetData`,
`useRuntimeFilters` and `emitFilter` for its placements.
- **Extensions to SIP-231** (see "Changes this SIP asks of SIP-231"): the
`x-runtime` keyword, `runtime_props` on data requests, and the
`emitRuntimeProps` / `useRuntimeProps` hooks (§5).
- **Realtime:** `entity.changed` with `entity_type: "canvas"`.
- **Security:** `Canvas` permission resource (`can_read`, `can_write`); a
`canvas` guest-token resource type (M2).
- **Configuration:** `CANVAS` feature flag,
`SUBJECTS_RELATED_TYPES_CANVASES`.
- **Models:** `canvases`, `canvas_ops`, `canvas_editors`, `canvas_viewers`;
permalinks use the existing key-value store.
- **Unchanged:** dashboards, `position_json`, `json_metadata`, charts and
their APIs.
</details>
### Changes this SIP asks of SIP-231
These need SIP-231's agreement; the rest of this SIP builds on SIP-231 as
written.
- **Runtime choices.** `x-runtime` in the documented keyword list, and
`runtime_props` beside `runtime_filters` on the instance and placement data
routes, with `emitRuntimeProps` and `useRuntimeProps` beside `emitFilter` and
`useRuntimeFilters` (§5).
- **One flag.** Both SIPs share `CANVAS` (§8), and it turns on by default
only when both reach parity, rather than at SIP-231's M2 alone.
### New dependencies
None.
### Migration Plan and Compatibility
Nothing existing changes until deployments opt in; all new tables, routes
and permissions are additive.
1. **M1: Canvas.** Behind the `CANVAS` flag, shared with SIP-231: the
document, operations including `promote` and `inline`, concurrency, scopes, the
listener bus with SIP-231's hooks, permalinks, snapshot export, rendering, the
MCP tools, and core containers. Ships alongside SIP-231 M1.
2. **M2: Parity.** Widgets for the chart and filter types dashboards use;
export and import (a canvas bundle with its persisted instances); embedding;
canvas reports and thumbnails (§12); direct editing on the page. Reached
together with SIP-231's M2, after which the shared flag turns on by default.
3. **M3: Opt-in conversion.** "Convert to canvas" copies a dashboard into a
canvas and leaves the original; a conversion report lists anything not carried
over. Layout maps one to one (`ROW` widths ×2 to `colSpan`, `COLUMN` to
`group`, `TABS`/`TAB` to `tabs`/`tab`), native filters become filter widgets
with their scope as `auto` or an override, and `json_metadata` settings map to
`settings`.
4. **M4: Canvas by default.** New dashboards are canvases; bulk conversion
for the rest.
5. **M5: Retirement.** Dashboard URLs redirect; the old format and renderer
are removed in a major release, recorded in `UPDATING.md`.
Each phase is gated on its milestone, not a date.
### Rejected Alternatives
<details>
<summary>Alternatives considered</summary>
- **Building on `dashboards.position_json`.** It keeps whole-document
writes, client-side grammar and a dashboard-specific component set; conversion
is a one-way copy instead.
- **UUID placement ids.** They cost agents a third more per edit for no
reliability gain, and nothing needs them to be global: an inline instance's
identity is already the canvas plus its placement.
- **Server-generated ids only.** No references between ops in one request,
and retried adds duplicate.
- **A nested tree, or `parents` arrays.** A flat map with `children` gives
each placement one address and one source of structure.
- **Whole-document `PUT`, JSON Patch over the document, or key-value drafts
of the canvas.** Named ops validate intent and make overlap detection precise;
JSON Patch is kept for what it suits, a single instance's props (`patch_props`).
- **Whole-document revision checks, per-placement locks, or CRDT/OT.** A
revision plus a touch log merges independent edits with a small, server-side
model.
- **Filter scope as explicit target lists, or owned by the filter widget's
props.** Scope belongs to placements (SIP-231 §9), and tree-derived defaults
follow the layout without upkeep; explicit lists remain available as `custom`.
- **Upgrading the document on read in code.** Format changes use Alembic
migrations, so the table never holds mixed versions; widget props, which change
on each widget's own schedule, use SIP-231's in-memory migrators.
- **Declaring filter dependencies for cascading.** Scope already says which
filters reach a filter, so a second dependency graph could only disagree with
it.
- **Filter state in the URL itself, or in the document.** URLs outgrow
browser limits with a few multi-select filters, and state in the document would
change the canvas for every viewer; a permalink key keeps both small.
- **Persisted instances by default.** Every text block and container would
be a row with its own permissions, and a canvas couldn't travel as one
document; promote covers sharing.
- **Query results in snapshots.** Opening a snapshot would show data under
the exporter's access and row-level security, not the reader's.
- **Customizations as runtime filters.** A group-by changes what is grouped,
which an additive filter can't express; letting callers send arbitrary
group-bys would let a viewer reshape the query. `x-runtime` limits choices to
what the stored instance offers.
- **Validating the whole document on every write.** A tightened widget rule
would then block unrelated edits.
</details>
### Prototype
The
[`msyavuz/feat/canvas`](https://github.com/apache/superset/tree/msyavuz/feat/canvas)
branch implements this proposal on top of the SIP-231 prototype's widget
framework: the document, readable placement ids, inline placements and
`set_props`, widget behavior and UI declarations with the core containers,
scopes, concurrency and live updates, the REST API and MCP tools behind
`CANVAS`, and a read-only canvas page that renders the core containers and
markdown.
The prototype's widget framework is the SIP-231 prototype, so it differs
from SIP-231 in names: widgets register with `@widget(widget_type=…)` rather
than `id`, the API is `/api/v1/widgets` with `Chart` permissions rather than
`/api/v1/widget` with `Widget`, its schema routes are `/types` and
`/type/<id>/control-schema`, its discovery tool is `list_widget_types` rather
than `list_widgets`, and `CANVAS` gates its widget API and tools as well as the
canvas. Renderers register through a canvas-only
`canvas.registerWidgetRenderer` without schema versions, and receive props,
instance ids and filter values as props (`filters`, `setFilterValue` and so on)
instead of through hooks. `add` and `set_props` validate props against the
schema only.
Not yet built:
- **Promote, inline and snapshot export** need the `WidgetInstance` table.
- **Persisted placements** are modelled and validated through
`InstanceResolver`, but no resolver is registered until the `WidgetInstance`
table exists, so adding one is refused.
- **The placement data endpoint** needs `QueryMixin` and the server-side
query pipeline.
- **Chart and filter renderers** need `useWidgetData` and field options;
until then chart and `filter.select` placements render a placeholder, so
filters can't yet be driven from the page.
- **Permalinks**, SIP-231's hooks and renderer registry, per-placement
datasources for the bus, and `x-runtime` / `runtime_props` for customizations.
- **`patch_props`**, `get_widget_info`, `get_widget_field_options`, and
validation beyond the props schema (datasource access, columns and metrics,
queries).
### Open questions
1. **Changes needed from SIP-231.** Runtime choices for customizations
(`x-runtime`, `runtime_props`) and one shared flag; see "Changes this SIP asks
of SIP-231".
2. **Unfinished instances.** Every write is validated in full, so a chart
can't be built up field by field on a canvas. Either writes must carry complete
props (agents and the property panel send whole, valid props), or a placement
may hold an incomplete instance with a visible "not configured" state that
renders no data.
3. **Previewing before it's live.** Every accepted op is visible to all
viewers, so an agent or person can't look at a configuration's data before
others see it. Options: a preview data route that takes props without storing
them, or accepting that iteration happens live (see 4).
4. **Live layout edits.** Every accepted op is the stored canvas, so placing
a widget can push others down for every viewer at once. An edit mode would
batch a person's changes before others see them, at the cost of the "no Save"
model agents rely on.
5. **Op log visibility.** Viewers can read operations from before they had
access through `…/definition/changes`. Options: filter the log by the viewer's
access date, or return only revisions and have clients refetch.
6. **History and undo.** The op log records every change with its author,
but keeps only 500 revisions and canvases aren't versioned. Decide whether
history comes from the op log, from row versioning, or both, and whether undo
is an inverse op.
7. **Filters across datasources.** A filter reaches only placements on its
own datasource. A later, additive field on scope overrides could map across
datasources, either by column name or by explicit column mapping.
8. **Conversion from dashboards.** Which dashboard features that a canvas
doesn't model (legacy filter scopes, `expanded_slices`, `async_mode`) block a
dashboard from converting, and which are only listed in the conversion report.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]