feat(dashboard-v2): introduce the dashboard editing UI and layout modes - #42865
Draft
EnxDev wants to merge 19 commits into
Draft
feat(dashboard-v2): introduce the dashboard editing UI and layout modes#42865EnxDev wants to merge 19 commits into
EnxDev wants to merge 19 commits into
Conversation
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>
Contributor
|
Bito Automatic Review Skipped - Branch Excluded |
✅ Deploy Preview for superset-docs-preview ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
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
TESTING INSTRUCTIONS
Automated tests:
Manual verification:
ADDITIONAL INFORMATION