Summary
Introduce a small, pluggable value-rendering architecture for the results area so that different value shapes — relational scalars, JSON documents/objects, key-value pairs, and future shapes — are each displayed with an appropriate, well-formatted renderer, instead of the current single "stringify-and-truncate" path.
This is motivated by the multi-model nature of the database fleet (especially the embedded libredb provider, which surfaces relational tables, document collections, and raw key-value namespaces), but the design is provider-agnostic: it is driven by the shape of the value, not the provider type.
Background — current state
Today the results grid renders every value through one type switch and the row detail sheet shows each field as a single collapsed line:
src/components/results-grid/utils.ts — formatCellValue(value) returns { display, className }. Objects are JSON.stringify'd compactly; everything else is String(value). No notion of a "JSON value", "KV pair", or any richer rendering.
src/components/ResultsGrid.tsx — cells are truncated single lines (correct for a scannable grid).
src/components/results-grid/RowDetailSheet.tsx — opens on row click; renders each field's value in a <p className="break-all">, so a multi-line / pretty-printed JSON value collapses to one line and is hard to read.
Concretely: a libredb query like prefix users: returns rows whose value column is a (pretty-printed) JSON string. In the grid the cell shows {"id": "1", "name": "... (truncated); in the detail sheet the newlines collapse, so the JSON is still not readable. MongoDB object values and SQL JSON columns have the same problem.
Problem
As more value shapes flow through the same grid (relational rows, JSON documents, KV entries, Redis structures), a single formatCellValue switch does not scale and produces poor UX for non-scalar values. There is no clean extension point to add a new renderer (e.g. a JSON tree, a KV pair view) without editing the central switch — i.e. it violates open-closed and will accrete if (type === ...) branches.
Goals
- A pluggable renderer architecture for result values, used consistently by both the grid cell (compact form) and the row detail sheet (full form).
- Shape-driven, not provider-driven: a value is classified into a kind (e.g.
scalar, json, kv, null) by inspecting the value; renderers are selected by kind. No === 'libredb' / provider-id checks in the rendering layer.
- Open-closed: adding a new renderer (new kind) is a new module + a registry entry, with no edits to existing renderers.
- Backwards compatible: scalars (strings, numbers, booleans, NULL) render exactly as they do today; this is purely additive for richer shapes.
- Proper, readable formatting for JSON/object values (pretty-printed, monospace, whitespace preserved; ideally a collapsible tree in the detail view).
Non-goals
- No change to query execution, the provider contracts, or the data returned by providers.
- No provider-specific behavior in the rendering layer (capabilities/labels may hint a preferred kind in future, but classification is value-shape-first).
- Not a full data-grid rewrite —
ResultsGrid virtualization, sorting, filtering, masking, and editing stay as they are; only the per-value rendering is abstracted.
Proposed architecture
A Strategy + small registry, living under src/components/results-grid/renderers/:
-
Value classification — classifyValue(value): ValueKind
null — null/undefined
scalar — string / number / boolean
json — an object/array, or a string that parses as JSON object/array
kv — a { key, value }-shaped row (the libredb/Redis raw layer), if we choose to render KV rows specially
- extensible: future kinds (e.g.
geo, binary, vector) slot in here.
-
Renderer interface — each renderer declares the kind it handles and provides two render functions so the grid and detail sheet share one implementation:
interface ValueRenderer {
kind: ValueKind;
/** Compact, single-line form for a grid cell (already truncated by the cell). */
renderCompact(value: unknown): { display: string; className: string };
/** Full form for the detail sheet (may be multi-line / interactive). */
renderDetail(value: unknown): React.ReactNode;
}
-
Registry — getRenderer(kind): ValueRenderer, with a scalarRenderer fallback. formatCellValue becomes a thin adapter over classifyValue + renderCompact (preserving today's output for scalars). The detail sheet calls classifyValue + renderDetail.
-
Initial renderers
scalarRenderer — exactly today's formatCellValue behavior (no visual change).
jsonRenderer — compact: a one-line stringify; detail: pretty-printed JSON.stringify(value, null, 2) in a whitespace-pre-wrap monospace block (a follow-up can upgrade this to a collapsible JSON tree).
- (optional, phase 2)
kvRenderer — a two-column key/value presentation for raw KV rows.
-
Masking — masking remains the outer concern: a masked value short-circuits to the masked string before the renderer is chosen (as it does today), so renderers never see sensitive values.
This keeps ResultsGrid and RowDetailSheet agnostic: they ask the registry for a renderer and use it. Adding a richer view later (JSON tree, KV table, etc.) is a new renderer module + one registry line.
Integration points
src/components/results-grid/utils.ts — formatCellValue reimplemented as classifyValue + getRenderer(...).renderCompact.
src/components/results-grid/RowDetailSheet.tsx — per-field value uses getRenderer(...).renderDetail.
src/components/ResultsGrid.tsx — unchanged call sites (still calls formatCellValue); only the helper's internals change.
- New:
src/components/results-grid/renderers/ (classifier, registry, renderer modules).
Acceptance criteria
- Scalars (string/number/boolean/NULL) render identically to today in both grid and detail sheet (snapshot/visual parity).
- A JSON object/array value, or a JSON-parseable string value, renders pretty-printed and readable in the detail sheet (whitespace preserved), and as a compact one-liner in the grid cell.
- Adding a new renderer requires only a new module + a registry entry — no edits to existing renderers or to
ResultsGrid/RowDetailSheet logic.
- No provider-type checks (
=== 'libredb', etc.) in the rendering layer; selection is by value kind.
- Masking, copy, sorting, filtering, virtualization, and inline editing continue to work unchanged.
- Unit tests for
classifyValue and each renderer's renderCompact; component test parity for the detail sheet.
Implementation sketch (suggested tasks)
Testing
- Unit:
classifyValue across null/scalar/object/array/JSON-string/non-JSON-string; each renderer's compact output.
- Component:
RowDetailSheet renders a JSON value as a formatted block and a scalar as plain text; masking still applies.
- Regression: existing
ResultsGrid / RowDetailSheet tests stay green (scalar parity).
Out of scope / future ideas
- Collapsible/interactive JSON tree, search-within-value, and per-cell "expand" affordance in the grid.
- A dedicated KV two-pane view and Redis-structure (hash/list/set) renderers.
- Capability-hinted default kind (a provider could suggest a preferred renderer via
ProviderCapabilities), still overridable by value shape.
References
src/components/results-grid/utils.ts (formatCellValue)
src/components/results-grid/RowDetailSheet.tsx
src/components/ResultsGrid.tsx
src/lib/db/providers/embedded/libredb.ts (renderValue already pretty-prints JSON values; this issue is about displaying such values well)
docs/DATABASE_PROVIDERS.md (provider architecture / Strategy Pattern precedent)
Summary
Introduce a small, pluggable value-rendering architecture for the results area so that different value shapes — relational scalars, JSON documents/objects, key-value pairs, and future shapes — are each displayed with an appropriate, well-formatted renderer, instead of the current single "stringify-and-truncate" path.
This is motivated by the multi-model nature of the database fleet (especially the embedded
libredbprovider, which surfaces relational tables, document collections, and raw key-value namespaces), but the design is provider-agnostic: it is driven by the shape of the value, not the provider type.Background — current state
Today the results grid renders every value through one type switch and the row detail sheet shows each field as a single collapsed line:
src/components/results-grid/utils.ts—formatCellValue(value)returns{ display, className }. Objects areJSON.stringify'd compactly; everything else isString(value). No notion of a "JSON value", "KV pair", or any richer rendering.src/components/ResultsGrid.tsx— cells are truncated single lines (correct for a scannable grid).src/components/results-grid/RowDetailSheet.tsx— opens on row click; renders each field's value in a<p className="break-all">, so a multi-line / pretty-printed JSON value collapses to one line and is hard to read.Concretely: a
libredbquery likeprefix users:returns rows whosevaluecolumn is a (pretty-printed) JSON string. In the grid the cell shows{"id": "1", "name": "...(truncated); in the detail sheet the newlines collapse, so the JSON is still not readable. MongoDB object values and SQL JSON columns have the same problem.Problem
As more value shapes flow through the same grid (relational rows, JSON documents, KV entries, Redis structures), a single
formatCellValueswitch does not scale and produces poor UX for non-scalar values. There is no clean extension point to add a new renderer (e.g. a JSON tree, a KV pair view) without editing the central switch — i.e. it violates open-closed and will accreteif (type === ...)branches.Goals
scalar,json,kv,null) by inspecting the value; renderers are selected by kind. No=== 'libredb'/ provider-id checks in the rendering layer.Non-goals
ResultsGridvirtualization, sorting, filtering, masking, and editing stay as they are; only the per-value rendering is abstracted.Proposed architecture
A
Strategy+ smallregistry, living undersrc/components/results-grid/renderers/:Value classification —
classifyValue(value): ValueKindnull— null/undefinedscalar— string / number / booleanjson— an object/array, or a string that parses as JSON object/arraykv— a{ key, value }-shaped row (the libredb/Redis raw layer), if we choose to render KV rows speciallygeo,binary,vector) slot in here.Renderer interface — each renderer declares the kind it handles and provides two render functions so the grid and detail sheet share one implementation:
Registry —
getRenderer(kind): ValueRenderer, with ascalarRendererfallback.formatCellValuebecomes a thin adapter overclassifyValue+renderCompact(preserving today's output for scalars). The detail sheet callsclassifyValue+renderDetail.Initial renderers
scalarRenderer— exactly today'sformatCellValuebehavior (no visual change).jsonRenderer— compact: a one-line stringify; detail: pretty-printedJSON.stringify(value, null, 2)in awhitespace-pre-wrapmonospace block (a follow-up can upgrade this to a collapsible JSON tree).kvRenderer— a two-column key/value presentation for raw KV rows.Masking — masking remains the outer concern: a masked value short-circuits to the masked string before the renderer is chosen (as it does today), so renderers never see sensitive values.
This keeps
ResultsGridandRowDetailSheetagnostic: they ask the registry for a renderer and use it. Adding a richer view later (JSON tree, KV table, etc.) is a new renderer module + one registry line.Integration points
src/components/results-grid/utils.ts—formatCellValuereimplemented asclassifyValue+getRenderer(...).renderCompact.src/components/results-grid/RowDetailSheet.tsx— per-field value usesgetRenderer(...).renderDetail.src/components/ResultsGrid.tsx— unchanged call sites (still callsformatCellValue); only the helper's internals change.src/components/results-grid/renderers/(classifier, registry, renderer modules).Acceptance criteria
ResultsGrid/RowDetailSheetlogic.=== 'libredb', etc.) in the rendering layer; selection is by value kind.classifyValueand each renderer'srenderCompact; component test parity for the detail sheet.Implementation sketch (suggested tasks)
classifyValue+ValueKindand unit tests.ValueRendererinterface + registry (getRenderer) with a scalar fallback.formatCellValuebehavior intoscalarRenderer; reimplementformatCellValueas an adapter (no visual change — verify with tests).jsonRenderer(compact one-liner + pretty<pre>detail).RowDetailSheetto userenderDetail.kvRendererand/or a collapsible JSON tree viewer in the detail sheet.Testing
classifyValueacross null/scalar/object/array/JSON-string/non-JSON-string; each renderer's compact output.RowDetailSheetrenders a JSON value as a formatted block and a scalar as plain text; masking still applies.ResultsGrid/RowDetailSheettests stay green (scalar parity).Out of scope / future ideas
ProviderCapabilities), still overridable by value shape.References
src/components/results-grid/utils.ts(formatCellValue)src/components/results-grid/RowDetailSheet.tsxsrc/components/ResultsGrid.tsxsrc/lib/db/providers/embedded/libredb.ts(renderValuealready pretty-prints JSON values; this issue is about displaying such values well)docs/DATABASE_PROVIDERS.md(provider architecture / Strategy Pattern precedent)