Skip to content

Result rendering architecture: pluggable value renderers for table / JSON / KV (and future shapes) #96

Description

@cevheri

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.tsformatCellValue(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/:

  1. Value classificationclassifyValue(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.
  2. 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;
    }
  3. RegistrygetRenderer(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.

  4. 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.
  5. 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.tsformatCellValue 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)

  • Add classifyValue + ValueKind and unit tests.
  • Add ValueRenderer interface + registry (getRenderer) with a scalar fallback.
  • Port current formatCellValue behavior into scalarRenderer; reimplement formatCellValue as an adapter (no visual change — verify with tests).
  • Add jsonRenderer (compact one-liner + pretty <pre> detail).
  • Wire RowDetailSheet to use renderDetail.
  • (Phase 2) kvRenderer and/or a collapsible JSON tree viewer in the detail sheet.
  • Tests + docs note.

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions