You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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)
(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.
Duplicate canonical URL → the RegistryError::DuplicateUrl that register() actually returns.
type = composite with no component.
Missing code / url / base.
Warnings
Empty expression → the extractor indexes zero rows and every search on the parameter silently returns empty.
type = reference with no target → chained search can't resolve the referenced type.
Informational
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-schemeand 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
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?
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.
Config source is in the enum but unused by this mockup. Is it live?
Version scoping. R4/R4B/R5/R6 ship different registries; the version pill must re-key the whole screen.
Editing expression has real blast radius. Changing it invalidates already-extracted index rows. Does saving trigger a reindex, and is that a background job?
Summary
A viewer and editor for
SearchParameterresources: 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.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 literallybase → code → param. The rail is a direct view of the outer map. SelectingPatientnarrows 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:
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:Storedparameter with the samecodeand a fresh canonical URL.The editor is a right-hand panel: code, canonical URL, type, base types (tag input), FHIRPath expression, description, plus
targetfor reference params and read-onlycomponentdisplay 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)
(base, code)collision with a different URL. This is a genuine, verified footgun.register()(registry.rs:293) rejects duplicate URLs — butregister_internalindexesparams_by_typeby(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.codeoverride that reproduces this, flags both rows with aconflictchip, and blocks the save. This alone justifies the screen.RegistryError::DuplicateUrlthatregister()actually returns.type = compositewith nocomponent.code/url/base.Warnings
expression→ the extractor indexes zero rows and every search on the parameter silently returns empty.type = referencewith notarget→ chained search can't resolve the referenced type.Informational
ofType(T)/as T) get a note that the extractor'srewrite_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.csstokens verbatim (--accent: #33b8ff, the[data-theme="dark"]block), vendored Figtree inlined as a data URI since the Artifact CSP blocks font CDNs.--ok/--danger/--warnare added as semantic tokens held off the accent hue, so aconflictchip never reads as branding.Both themes are defined through
prefers-color-schemeand thedata-themeoverride, matchingassets/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
data/search-parameters-r4.jsonhasstatus: "draft"— all 1,375 of them.load_from_spec_file()promotes draft → active on load (load_embedded()does not), andget_active_params()filters onstatus.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?.insert()behavior above, aStoredparameter 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.Configsource is in the enum but unused by this mockup. Is it live?expressionhas real blast radius. Changing it invalidates already-extracted index rows. Does saving trigger a reindex, and is that a background job?Suggested acceptance criteria
(base, code)collision detection, with a decision on Q2 recorded.Embeddedparams stay read-only with an override path.References
crates/fhir/src/search/registry.rs—register()(:293),register_internal()(:302),SearchParameterSource(:58),get_active_params()(:254)crates/persistence/src/search/extractor.rs—rewrite_choice_types()data/search-parameters-{r4,r4b,r5,r6}.json