Skip to content

SearchParameter viewer & editor UI: Resource Filter rail, faceted browse, CRUD with registry-aware validation #238

Description

@smunini

Summary

A viewer and editor for SearchParameter resources: browse the 1,375 parameters the server actually resolves searches against, filter down to the ones you care about, and create / update / delete tenant-scoped ones.

Interactive mockup: docs/mockups/search-parameter-editor.html — self-contained, open locally in a browser; PNG preview alongside (PR #247)

Loaded with the real R4 registry from data/search-parameters-r4.json — 1,375 parameters across 135 base resource types — plus two clearly-marked synthetic tenant parameters to exercise the write paths. Nothing persists.

Depends on #235 (Store SearchParameters in storage; make the in-memory registry a TTL cache over it). Until storage is the source of truth, the write half of this screen has nowhere to write. The read half works today.

Reusing the Resource Filter

The left rail is the Resource Filter from the Resources screen, reused unchanged in behavior: a search box over resource types, an All types row, a Recently used group, and a live count next to every type. Here it filters the parameter list by base.

This is the right primitive to borrow, because the registry is already keyed this way — params_by_type: HashMap<String, HashMap<String, Arc<SearchParameterDefinition>>> is literally base → code → param. The rail is a direct view of the outer map. Selecting Patient narrows 1,375 parameters to the ~40 that resource actually supports.

Two facet rows sit above the table (type and source), scoped to the current base type and showing live counts:

Parameter type Count
token 536
reference 472
string 133
date 109
composite 46
uri 45
quantity 27
number 6
special 1

CRUD model

SearchParameterSource (crates/fhir/src/search/registry.rs:58) already has the three cases this UI needs — Embedded, Stored, Config — so the editor keys off it rather than inventing a concept:

  • Embedded (spec, compiled in from the data file) — read-only. The action offered is Create override: clone it into a Stored parameter with the same code and a fresh canonical URL.
  • Stored (POSTed / tenant-scoped) — full create, update, delete. Delete is an inline confirm, never a browser dialog.

The editor is a right-hand panel: code, canonical URL, type, base types (tag input), FHIRPath expression, description, plus target for reference params and read-only component display for composites.

Validation is the actual value here

The mockup lints the draft parameter live, and every rule is grounded in real behavior rather than invented:

Errors (block save)

  1. (base, code) collision with a different URL. This is a genuine, verified footgun. register() (registry.rs:293) rejects duplicate URLs — but register_internal indexes params_by_type by (base, code) using .insert() (line 311). So a different URL with the same (base, code) silently overwrites the spec parameter, and the loser just stops resolving. Nothing errors. Nothing logs. Searches quietly change meaning.
    The mockup ships a synthetic Observation.code override that reproduces this, flags both rows with a conflict chip, and blocks the save. This alone justifies the screen.
  2. Duplicate canonical URL → the RegistryError::DuplicateUrl that register() actually returns.
  3. type = composite with no component.
  4. Missing code / url / base.

Warnings

  1. Empty expression → the extractor indexes zero rows and every search on the parameter silently returns empty.
  2. type = reference with no target → chained search can't resolve the referenced type.

Informational

  1. Choice-type expressions (ofType(T) / as T) get a note that the extractor's rewrite_choice_types() maps them to the concrete element (valueQuantity, occurrenceDateTime) before evaluating against raw stored JSON — because the extractor runs against schema-less JSON, not a typed model.

Design notes

Same design system as the Compartment Editor (#237) and the existing crate: crates/ui/assets/app.css tokens verbatim (--accent: #33b8ff, the [data-theme="dark"] block), vendored Figtree inlined as a data URI since the Artifact CSP blocks font CDNs. --ok / --danger / --warn are added as semantic tokens held off the accent hue, so a conflict chip never reads as branding.

Both themes are defined through prefers-color-scheme and the data-theme override, matching assets/theme.js. The list renders at most 250 rows and says so explicitly rather than silently truncating — the real screen should paginate.

Per crate rules, real markup lives in templates/; the mockup's JS is prototype scaffolding, and htmx would swap the detail panel as a fragment.

Open questions

  1. Status display is a small lie we should decide about. Every parameter in data/search-parameters-r4.json has status: "draft" — all 1,375 of them. load_from_spec_file() promotes draft → active on load (load_embedded() does not), and get_active_params() filters on status.is_usable(). The mockup shows the runtime status (active) with a hint explaining the promotion. Is that the right call, or should the UI show stored status and make the promotion explicit?
  2. Does an override actually override? Given the .insert() behavior above, a Stored parameter with the same (base, code) wins or loses depending purely on registration order. If overrides are a supported feature, that ordering needs to be defined, not incidental. If they aren't, the collision should be a hard error at load time.
  3. Deleting a parameter that a saved query uses (User settings: maintain a list of saved FHIR queries by resource type, sortable by last access #234) — warn, block, or orphan?
  4. Config source is in the enum but unused by this mockup. Is it live?
  5. Version scoping. R4/R4B/R5/R6 ship different registries; the version pill must re-key the whole screen.
  6. Editing expression has real blast radius. Changing it invalidates already-extracted index rows. Does saving trigger a reindex, and is that a background job?

Suggested acceptance criteria

References

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Fields

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