Skip to content

feat(components): shared grid, detail panel, stats tiles and entity modals - #7

Open
laksamanakeris wants to merge 7 commits into
mainfrom
feat/phase2-components
Open

laksamanakeris wants to merge 7 commits into
mainfrom
feat/phase2-components

Conversation

@laksamanakeris

Copy link
Copy Markdown
Collaborator

Builds the shared pieces every screen tier composes from, once: a data grid, a detail panel, stats tiles, and the 8 entity and confirm-delete modals. Still 100% mock. No modal writes anything yet.

Size 7 commits, 25 files, +1430 / −120, 5 new files
Tests 241 → 261
Gate fmt, clippy -D warnings, nextest, all clean
Checked in the app All 7 entity forms opened from the Explorer, one per engine. Preferences deep-link and pane switching. Cancel, ×, and backdrop close.
Risk Additive, except ResultSet (fields now private), ModalKind::Preferences (now carries a pane), and a new StudioError::MalformedRows variant. The compiler flags every site of all three.

The design choice to accept or reject

The modals are static, and their Save and Delete buttons only close them.

Two things a real submit needs do not exist yet:

  • A seam write. mark_all_read is the only write on ConnectionService. Adding upsert_record / delete_record now would fix their shape before any screen selects a record, and the seam is an ask-first change.
  • A target record. Nothing selects one until the Explorer's master-detail lands. So the entity variants of ModalKind are unit variants, and each body shows the mockup's fixture record. For example, the Strict form reads "orders / 442003" whichever collection is open.

Both arrive with the Explorer work. The record then travels in ModalKind, and submit goes through use_action. Every placeholder is marked SEAM-UNWIRED. If you want the write methods designed now instead, say so here, because the Explorer builds on this.

Where to spend review time

1. models/workbench.rs, ResultSet::new, and the new MalformedRows error. Fields are private now. The constructor rejects a row whose width differs from the header, and a duplicate row id. Without the width check, a decoder that drops one cell renders every later value under the wrong heading. A user cannot tell that from real data. Without the id check, duplicate Dioxus keys panic in debug builds and reuse the wrong node in release builds. Deserialize is gone because it would bypass the check.

Both rejections raise a new StudioError::MalformedRows { reason }, non-retriable. UnexpectedColumns means a missing column (the SHOW session-variable fallback), and reusing it pointed the user at the columns when the fault was in the rows. The decoder's short-row check in services/decode.rs moves to MalformedRows for the same reason. Missing-column checks keep UnexpectedColumns. A new StudioError variant touches the type every seam method returns, so confirm you are fine with it.

2. views/explorer/view.rs, insert_actions. The header's create buttons now follow the storage mode, as the mockup does:

  • Document, Strict, Vector, KV: + Insert
  • Graph: + Node and + Edge
  • Spatial: + Feature
  • Timeseries (append-only) and FTS (writes happen on the parent): none

It is an exhaustive match, so a new StorageMode does not compile until someone decides its create flow.

3. modals/preferences.rs, one source of truth for the open pane. ModalKind::Preferences(PrefsPane) carries the pane, and a sidebar click re-sets the modal signal. No second local signal exists that a deep-link could disagree with. The avatar popover's "Keyboard shortcuts" and "About" items now land on those panes. The old string keys and their _ => fallback are gone.

One thing I want your call on

Where do the create buttons live? This PR puts them in the viewer header, next to Schema, Indexes and Export, matching the mockup's per-engine toolbars. The plan for the Explorer folds header actions into a single ⋯ more-actions button. If that stands, only the render site of insert_actions moves. The mapping stays. Worth settling before the Explorer work starts.

Smaller changes

  • Modal takes an optional width in px for the mockup's per-form widths (540 / 560 / 640 / 420).
  • Confirm-delete keeps "Delete permanently" disabled until the input is delete. Surrounding whitespace is forgiven. Case is not.
  • The edge form's From / type / To row uses a three-column form-row. The mockup's two-column row wrapped "To" onto its own line. This is the one deliberate divergence from the mockup.
  • DetailPanel and StatCard / StatSection are not used by a screen yet. The engine viewers adopt them when their list-detail layout and stats tab land.
  • Launch with cargo run -p nodedb-studio to review. Running target/debug/nodedb-studio directly from the workspace root rendered with no stylesheet. The cause is unconfirmed, and this PR does not change it.

One tabular renderer for the seam's ResultSet: column headers from the
result, one keyed row per RecordRow, one cell per string. Pure
presentation. Every wire scalar already arrives as a display string, so
the grid never parses. Sorting or typed comparison cannot be recovered
from display strings like "14,820" or "—", so that waits on the model
carrying raw values rather than on this component.

ResultSet is now rectangular and uniquely keyed by construction. Its
fields are private and ResultSet::new rejects a row whose width differs
from the header, or a duplicate row id, with UnexpectedColumns, the same
error the seam decoder raises for a short row. Without the width check a
decoder that dropped a cell would render every later value under the
wrong heading, which a user cannot distinguish from what the database
holds. Without the id check a decoder keying rows off a data column would
hand Dioxus duplicate sibling keys, which panic in debug and silently
reuse the wrong node in release. Both checks fail their tests when
removed. Deserialize is dropped from ResultSet since it would bypass the
constructor and nothing deserializes a model.

Headers are keyed by ordinal, not by name: SQL allows duplicate column
names and the same duplicate-key panic applies.

The Query workbench is the first consumer. Its inline table had hardcoded
headers and an unkeyed row loop, and its toolbar and footer repeated the
row count, elapsed time and scan size as literals. The rows are now a
ResultSet with seam-shaped ids and the counters derive from it. The view
is still a static placeholder: reading run_query through the seam also
needs the loading, empty and error states this view does not render, so
the placeholder carries an unwired marker naming that condition, and a
render test pins it at seven rows so a ragged edit fails the suite
instead of rendering a blank grid.

Render tests assert whole rows verbatim and contiguous, so column order,
row order and header-to-cell alignment are pinned rather than presence,
plus zero rows and duplicate column names.
One header/body/footer shell for the Explorer's 8 engine viewers. The
header (title, freshness chip, close) is identical across the mockup;
body and footer actions differ per viewer and arrive as slots, the same
shape LiveTail uses. Freshness is a display string plus a stale flag:
the staleness rule needs a fetch time the seam does not carry yet.
The close control is a button, not the mockup's span, for keyboard and
screen-reader access.
ModalKind gains the seven entity-form variants (document, strict,
vector, graph node, graph edge, KV, spatial) and ConfirmDelete, each
with a ModalHost arm. Bodies match the mockup and show its fixture
record. Save and Delete only close the modal: no seam method writes a
record yet, and nothing selects one until the Explorer's master-detail
lands. The variants carry no payload until then.

The Explorer header's create buttons now follow the storage mode:
+ Insert for document/strict/vector/KV, + Node and + Edge for graph,
+ Feature for spatial, none for timeseries (append-only) or FTS
(writes happen on the parent). The mapping is an exhaustive match in
insert_actions, so a new StorageMode must decide its create flow.

Confirm-delete keeps Delete permanently disabled until the typed word
is "delete"; surrounding whitespace is forgiven, case is not.

Modal takes an optional width in px for the mockup's per-form widths.
The edge form's From/type/To row uses a three-column form-row; the
mockup's two-column row wrapped To onto its own line.
ModalKind::Preferences now carries a PrefsPane, so a trigger picks the
pane it opens. The avatar popover's "Keyboard shortcuts" and "About"
items land on those panes instead of Appearance; every other trigger
(⌘, palette, connection manager) opens Appearance as before.

The open pane lives in the modal signal alone. A sidebar click re-sets
ModalKind::Preferences(pane) rather than updating a second, local
signal, so a deep-link and a click cannot disagree. The string keys and
their _ => fallback give way to exhaustive matches on PrefsPane.
The mockup's engine viewers share one stats pane: a grid of stat cards
(label, value, sub-line) and titled sections that frame a chart or a
breakdown. StatCard and StatSection own that markup and CSS once. The
chart inside a section differs per viewer and arrives as its body; the
viewers adopt these when their stats vtab lands.
Adding insert_actions_match_the_mockup_per_mode split the doc comment
from selection_is_kept_only_while_the_collection_exists, leaving the
new test with both explanations and the old one with none.
ResultSet::new and the decoder's Row::field raised UnexpectedColumns
for a ragged row or a duplicate row id. That variant describes a
missing column (the SHOW session-variable fallback), so the message
pointed the user at the columns when the fault was in the rows:
"unexpected result columns: expected [unique row ids], got [...]".

MalformedRows { reason } now carries those cases, non-retriable, with
a message that names the row: "result rows do not match their header:
row r2 has 1 cells, expected 2". Missing-column checks keep
UnexpectedColumns. Its dead_code allow is dropped: ResultSet no
longer constructs it, but clippy passes without the allow.
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