Skip to content

feat(dashboard-v2): introduce the dashboard editing UI and layout modes - #42865

Draft
EnxDev wants to merge 19 commits into
dashboard-v2from
enxdev/feat/ai-native-dashboards-ui-v1
Draft

feat(dashboard-v2): introduce the dashboard editing UI and layout modes#42865
EnxDev wants to merge 19 commits into
dashboard-v2from
enxdev/feat/ai-native-dashboards-ui-v1

Conversation

@EnxDev

@EnxDev EnxDev commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

SUMMARY

This PR expands Dashboard v2 from an assistant-first canvas into a complete editing workspace. Human edits and assistant-driven changes use the same dashboard provider, so both paths update the same in-memory dashboard tree.

The editing UI adds:

  • A dashboard header with an editable title, author metadata, publication state, and clearly disabled actions that are not supported by the prototype yet.
  • A resizable and collapsible editor panel with Building blocks, Properties, and Outline tabs.
  • A searchable, registry-backed block palette. Blocks can be added by clicking or dragged into the root or a nested canvas.
  • A block inspector for editing content, generated property forms, raw JSON properties, placement, sizing, and deletion.
  • JSON validation with Apply, Revert, and Copy actions.
  • Dashboard-level properties built from the existing dashboard properties sections.
  • A navigable outline that reflects nested dashboard structure, selects blocks, and scrolls them into view.
  • Consistent block chrome with readable titles, selection states, a single card frame, and direct removal controls.

This PR also adds three container layout modes:

  • grid: the existing compact, collision-aware layout and the default for layouts without an explicit mode.
  • free: allows blocks to overlap and provides controls for moving blocks to the front or back.
  • flex: supports row or column flow, wrapping, alignment, justification, proportional sizing, and drag-to-reorder.

NB: Dashboard v2 also registers browser-owned client tools and dashboard API for reading and editing the dashboard tree. Those are provisional, may be amended,

These tools let the assistant validate data bindings and add, update, move, resize, or remove blocks through the same APIs used by the editing UI.

Dashboard state remains in memory for this prototype. Save, History, Refresh, and other persistence-dependent actions remain disabled, I'll update those.

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

Screenshot 2026-08-06 230058 Screenshot 2026-08-06 230158 Screenshot 2026-08-06 230218 Screenshot 2026-08-06 230304 Screenshot 2026-08-06 230325 Screenshot 2026-08-06 230356

TESTING INSTRUCTIONS

Automated tests:

cd superset-frontend
npm test -- \
  src/pages/DashboardBuilderV2 \
  src/core/dashboard \
  src/core/chat

Manual verification:

  1. Open /dashboard/v2/new/.
  2. Edit the dashboard title and verify it is reflected in dashboard properties.
  3. Search and collapse sections in the Building blocks palette.
  4. Add blocks by clicking palette items and by dragging them onto the root and nested canvases.
  5. Select blocks from both the canvas and Outline and verify the Properties tab shows the selected block.
  6. Edit Markdown content and block properties using both the generated form and JSON editor.
  7. Verify invalid JSON cannot be applied, and test Apply, Revert, and Copy.
  8. Resize, collapse, and reopen the editor panel. Verify its resize handle also works with the keyboard.
  9. Select the dashboard root and switch between grid, free, and flex layouts.
  10. In free mode, overlap blocks and test Bring to front and Send to back.
  11. In flex mode, test direction, wrapping, alignment, justification, and drag-to-reorder.
  12. Delete blocks from both the canvas header and the Properties panel.
  13. If a compatible chat extension is available, ask the assistant to add or update a block and verify the editor updates immediately.

ADDITIONAL INFORMATION

  • Has associated issue:
  • Required feature flags:
  • Changes UI
  • Includes DB Migration (follow approval process in SIP-59)
    • Migration is atomic, supports rollback & is backwards-compatible
    • Confirm DB migration upgrade and downgrade tested
    • Runtime estimates and downtime expectations provided
  • Introduces new feature or API
  • Removes existing feature or API

EnxDev and others added 19 commits August 5, 2026 12:37
Three arrangements, one of which is what every dashboard already does.
`grid` is the default and means exactly what a container meant before
this field existed, so every stored node and every AI tool call that
omits `mode` behaves identically.

The other two are the ones that could not be expressed as a grid. `free`
because compaction belongs to the container rather than to any child's
coordinates: it reads the same four child fields `grid` does, so moving
between them never discards a position an author or an agent set. `flex`
because a proportional line has no cells to name — position there is
order in `children`, which is why the gesture that arranges one is a
reorder committing through the same `moveBuildingBlock` the tools call.
Without that, flex would be a mode you can see and cannot author in.

A flow is still not a mode, and the comment saying so is kept. So is the
one recording that `compactType={null}` displaces siblings without
bound — `free` pairs it with `allowOverlap`, which removes the collision
resolution rather than leaving it running with nothing to settle it, so
a free canvas never enters the path that was found to fail.

The switcher edits `layout.mode` through `updateLayout`, the same call
an agent makes, so asking for a free canvas and pressing Free are one
edit and the control shows whichever happened last.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Hiding it until something was on the canvas read as "nothing to arrange
yet". The practical effect was the opposite: /dashboard/v2/new/ opens
empty by definition, so the control was invisible at the one moment
someone would look for it.

Setting the arrangement before adding anything is also the ordinary way
round — whatever the assistant places next lands in the mode already
chosen, rather than being placed and then rearranged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The page was a canvas and a chat panel: everything on a dashboard had to
be asked for in words. It gains a header and a three-tab panel, so
placing a block, editing one and finding one are things an author can
also do directly.

Selection is what the panel needed and the provider did not have. It
lives there as host-internal state beside the revision counter, and for
the same reason: it belongs to one person looking at one screen, not to
the dashboard, so it is not on the public API. Putting it in the store
both layers already subscribe to beats threading it through the render
tree BuildingBlockView deliberately keeps ignorant.

The palette is the registry — `views.getViews('dashboard.buildingBlocks')`,
the same call that resolves a renderer — so registering a block makes it
placeable with no list here to keep in agreement. It shelves on the one
distinction this fork records: whether placing the type produces
something other blocks can go inside. There is deliberately no
Extensions shelf, because a registered View says nothing about who
contributed it and a dotted-id convention would be a guess dressed as a
fact.

Every field in Properties writes through updateLayout/updateProps, the
same two calls the client tools make, so a change made by hand and one
asked for in chat are the same edit arriving by different routes.

Most of the header is disabled on purpose. The builder holds its tree in
memory with no dashboard row behind it: nothing can be saved,
favourited, published or refreshed, and there is no history to step
through. Drawing them disabled says which parts of the product this page
is still missing; drawing them live and inert would teach something
false about all of them. The layout switcher is the exception, and it is
live precisely because its state is in the tree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The header had no name in it because this fork has nowhere to keep one:
no dashboard row, and a deliberate decision that a title is a `markdown`
block placed on the canvas like any other content.

That decision stands and this is a different thing. A markdown title is
content — arranged, positioned, and one block among many. A name is what
the dashboard is called, and it belongs to the dashboard rather than to
its contents.

So it is stored on the root node, which is the only node a fact about
the dashboard itself can belong to. Page state was the alternative and a
worse one: a name held there is invisible to the assistant, unreachable
by the client tools, and gone on the next navigation.

The draft commits on blur, because a name being typed is not a name and
a commit per keystroke is a revision tick per keystroke for everything
subscribed. An emptied field restores rather than writing the blank: a
stray select-all-and-delete must not silently leave the dashboard
nameless.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A block placed from the palette could not be authored. Markdown arrives
with no props at all, so the Content field — which waited for a `content`
key to already exist — never appeared, and the one prop that block reads
had no way in. Chart, table and metric tile were worse off still: their
dataBinding, echartsOptions and columnDefs had never had a hand-editing
path of any kind.

Properties now carries both. A prose field for the types whose renderer
reads plain text, offered whether or not there is any yet. And a JSON
editor over the node's whole props, which is the general answer and
general on purpose: every key any block reads, including whatever an
extension registers next year, without this panel learning a single type
— the exact knowledge BuildingBlockView is built not to have.

A draft is held until it parses and the author asks for it, so malformed
JSON never reaches a block and a half-typed edit is never taken away. A
key the author deleted is sent as `undefined`, which is as close to a
removal as a merge can express; omitting it would silently do nothing and
the block would go on rendering from the value it appeared to lose.

The panel is also set down from the tab bar. Flush against it, the first
line read as a caption belonging to the tabs rather than to the block it
names.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bar is chrome around the work rather than the work itself, and every
pixel it takes is one the canvas does not get.

Heights come from the theme's own control steps rather than literals, so
they track the scale the rest of the app is built on instead of drifting
from it. The icons and the title field come down with the buttons: a
control left at its old size beside smaller ones reads as a different
kind of thing rather than the same kind, larger.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three things, one of them a defect this shell introduced.

The palette rows carried a drag grip beside every label and no drag.
They are draggable now, onto any container rather than only the root: a
nested section is exactly where an author means to put something when
they drag it there, and the innermost container under the pointer takes
the drop rather than every ancestor claiming it. The payload is a
private type, so a dragged file or a selection of text from another
window is not read as a request to place a block.

Both ways of asking now go through one `placeBlock`. Two copies of what
a freshly placed block looks like is how a block dropped into a section
ends up subtly different from the same block clicked into it, and the
difference stays invisible until someone hits it.

Blocks gained a remove control in their top-right corner, shown while
the block is hovered or selected rather than permanently — a delete on
every block at all times is a row of delete buttons where a dashboard
should be. It is excluded from the grid's drag via `draggableCancel`,
because react-grid-layout starts a drag on a press anywhere in the block
it positions and aiming at the X would otherwise drag what it is
attached to. The root has none: removing it is refused by the provider,
so offering the button would be offering an error.

The palette's search field is also set down from the tab bar and in from
the panel edge, where flush against both it read as chrome around the
list rather than the way into it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every leaf block fills the box its placement wrapper hands it: a chart
measures that box to size its canvas, markdown scrolls inside it. In a
grid that box arrives as the explicit pixel width and height
react-grid-layout injects when it clones the block, and ChartBlock's own
comment already named the hazard -- a measured size that "collapses to
zero the way an unconstrained flex height could".

FlexCanvas positions its children itself and never handed the box down,
so every block was content-height instead. A chart's measured height
settled at its loading indicator, drawing a ~30px strip at the top of an
otherwise empty cell; markdown taller than its share painted over the
row beneath it.

The height a flex child reserves also disagreed with the grid's:
react-grid-layout counts the gaps *between* the spanned rows toward the
block, so the same rowSpan drew shorter in flex and switching mode
resized the whole canvas. resolveBlockHeightPx now carries that one
formula for both.
A block on the canvas said nothing about which block it was. The one
place it was named -- the Outline -- named it by rules of its own, so
the same chart could read "Sales by Territory" in the panel and nothing
at all on the canvas.

blockLabel holds those rules now, in one place both callers can reach: a
name the block's own content carries wins (a chart's ECharts title, a
metric tile's label, markdown's opening words) and only a block with
none falls back to the registered type name. Returned whole, since a row
in a panel and a header on a wide chart cut a long name at different
points.

The header carries that name on the left and the delete control on the
right, and the control no longer waits to be hovered: a control you have
to already know is there is a control most people never find. The root
gets no header -- it is the dashboard rather than something on it, and
the provider refuses to remove it.

A chart's name is authored in its ECharts option, so ChartBlock stops
ECharts drawing it: it belongs where every other block's name is, once,
rather than twice at two sizes.

The band comes out of the block's own box in pixels off a percentage
rather than by making the wrapper a flex column -- what a leaf block
does with that box is resolve `height: 100%` against it, and a chart
measures the result to size its canvas, so it wants a height there is no
question about.
Marking a block as selected is only half of reaching it. The rows this
panel exists for are the ones for blocks the canvas is not currently
offering -- something scrolled past, or nested inside a section further
down -- and clicking one of those left an author looking at an unchanged
canvas, with nothing to say the click had landed.

The block's own element already carries `data-node-id`, so the canvas
needs no wiring back to here: the outline finds the element and scrolls
it into view itself.

`nearest` rather than `center`, because this fires on every row and
reading down a list of blocks that are already in view should not drag
the canvas under them. A node can be in the tree without being rendered,
and nothing happens at all when the element is absent -- the selection
has been set by then either way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A free canvas is the one mode where blocks can overlap, and which of two
overlapping blocks won was not something an author could see, let alone
choose. react-grid-layout gives an overlapping child no `z-index` of its
own, so the browser fell back to tree order and the container's
`children` order silently became the paint order: a block earlier in the
array could not be brought forward by moving it, resizing it, or
selecting it. Dragging one over another appeared to work, because
react-grid-layout raises whatever is being dragged -- and then dropped it
back underneath.

`bringToFront` and `sendToBack` say it directly, and a drag that ends in
a free canvas says it too: releasing a block over another is the gesture
that means "in front", so `handleDragStop` raises what was dropped.

The two ends are the whole control on purpose. "Forward one" and "back
one" are the same call with an index arithmetic that only means anything
to someone already picturing the array.

Reordering a block used to move it: `moveBuildingBlock` reset `col`,
`row` and `colSpan` on every call, which is right when a block changes
parent and lands in a grid it has no coordinates in, and wrong when it
stays where it is. It is now guarded on the parent actually changing, so
raising a block leaves it where the author put it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The card -- background, border, rounded corners -- was drawn by each leaf
block, and a leaf begins below the header. So a block's top edge ran
between its name and its contents: the name sat outside the box it names
and read as a caption dropped over a separate card, with a seam across
the block a hand's width below the top.

Drawn from the wrapper instead, it encloses both. That is also the only
place it can be drawn from: whether a node has a header at all is
`BuildingBlockView`'s to know, not the leaf's, which is why four blocks
each opened with the same three lines. The header stops painting a
surface of its own, and no rule is added under it -- one unbroken card
with a name on it is how a chart card reads everywhere else.

It matters beyond the seam. On a free canvas blocks overlap, and
anything a block does not paint is a window onto whatever is behind it,
so a block raised to the front still showed the one behind through its
own header band. `overflow: hidden` keeps square content out of the
corners the frame rounds -- which also stops a block painting outside
the cell it was given, something nothing was clipping before.

The root keeps a frame and gains a gutter but no surface: it is drawn by
a grid that fills its box edge to edge, so it had no pixels of its own,
and the inset is what an author aims at to select the dashboard rather
than something on it. It carries no background because it is what
everything else is arranged on, not a card among them.

The name itself is drawn as a title rather than as a note about one. At
the small size in the secondary colour it read as an annotation hanging
above the block, and it is the first thing anyone scanning a canvas uses
to tell one block from the next.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Selecting the root offered the same three questions a block gets -- what
it renders, where it sits -- none of which the dashboard has an answer
to. What it does have is what it is called, who may see it, how it
looks, how often it refreshes; and those were reachable only from the
saved dashboard's properties modal, which this builder has no route to.

The panel now reuses that modal's six sections whole rather than
restating them, so the two are ways into one set of fields. Values live
on the root's props and commit when focus leaves the panel.

Arranging is the one thing a dashboard and a container have in common,
so the layout switcher moves here from the header, alongside the
columns, gap and row height it works with -- properties of the container
they are asked about, rather than a control on a bar above.

Which of those are shown now follows the mode, because a field the
renderer ignores is worse than a missing one: it accepts a value, writes
it to the node, and changes nothing, so an author concludes the layout
is broken rather than that the question did not apply. `col` and `row`
are grid coordinates and a flex line has no cells to hold them, so a
flex child is not asked. And `direction`, `wrap`, `justify` and `align`
-- documented on LayoutProps as flex-only and until now offered nowhere
at all -- are asked of a flex container, which could otherwise be chosen
and then not actually arranged.

Properties gains a form beside the JSON. No block type declares a
schema, so `inferPropsSchema` reads one off the values the block is
holding; a schema shipped with each registration would be better, and
this is what stands in until there is one -- a contributed block gets a
form on the same terms a built-in one does, with no list to keep
current. The two halves divide cleanly: JSON decides the shape, since it
alone can add or drop a key, and the form fills in the values. JSON is
what the panel opens on, because a block placed a moment ago has no
properties and so no fields.

The form must not sit under an antd Form: its controls render
`Form.Item name=...`, and a Form above them binds those items to its own
store, so the field accepts typing and the edit lands somewhere nothing
reads. It writes on change rather than on blur for a related reason --
JsonForms debounces what it reports by 10ms, and a blur fires before
that lands, saving the value as it stood a keystroke earlier.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The canvas is the work; this rail is how you act on it. An author
reading a dashboard at full width had no way to reclaim the 500px it
holds, short of dragging its edge all the way in and back out again.

The fold control rides the tab bar rather than sitting above it, because
closing the panel is done to the panel, and a row of its own for one
icon would cost that row's height on every screen that never uses it.

Closing is not resizing, so a closed panel keeps the width it was opened
at: a panel that reopened at the default would silently discard a width
the author had already chosen. The strip left behind is exactly as wide
as the one control on it and offers no edge to drag, since there is
nothing there to size and a draggable strip would be a third state.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bar above the canvas had collected everything: what the dashboard is
called, whether it is saved, and also how the canvas lays blocks out and
when it reloads. The last two are not chrome about the dashboard -- they
act on the blocks in front of you and are reached for while looking at
them -- so they move into the canvas's own top-left corner.

Arrange is a route rather than a second control. How a container lays
out its children is a property of that container, asked with the columns
and the gap it works alongside, and a copy of the switcher here would be
a second thing to keep agreeing with the first: it selects the root, and
the panel brings Properties forward on a selection it did not make.

That would have made the mode unreachable on a blank dashboard, where
the placeholder renders instead of the root and there is nothing to
select -- which is exactly when the mode is worth asking, since whatever
is placed next lands in the one already chosen. The placeholder is now
the dashboard, and selects it.

Refresh is the opposite: named, and honest that it cannot work. There is
no row behind this page and no query to re-run, so it says so rather
than doing nothing quietly. `Inert` is what says that in one place --
disabled, wrapped in a span so the tooltip survives the disabled button.

What is left on the bar is what the dashboard is, in the order it is
read: the name, then who owns it and when it last changed, then the
saving. History moves beside Save because that is what it is a history
of. Templates, History and Save are what an author leaves with and are
sized accordingly.

The canvas padding is two past the token so the corner controls clear
the frame the root draws inside it, written as an offset rather than a
literal so it still moves with the scale.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@bito-code-review

bito-code-review Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Bito Automatic Review Skipped - Branch Excluded

Bito didn't auto-review because the source or target branch is excluded from automatic reviews.
No action is needed if you didn't intend for the agent to review it. Otherwise, to manually trigger a review, type /review in a comment and save.
You can change the branch exclusion settings here, or contact your Bito workspace admin at evan@preset.io.

@netlify

netlify Bot commented Aug 6, 2026

Copy link
Copy Markdown

Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit 80a58e3
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6a74fa2eac1b6000082356bc
😎 Deploy Preview https://deploy-preview-42865--superset-docs-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant