feat(ui): Bulk Export workspace — scoped kick-off, filters, active exports - #538
feat(ui): Bulk Export workspace — scoped kick-off, filters, active exports#538angela-helios wants to merge 9 commits into
Conversation
…ports The pull-based companion to the Bulk Import workspace (#537), driving the server's own $export operation from the web UI and replacing the Export placeholder in the Batch & Data nav. - /ui/bulk-export: scope choice (everything / patients / group with a Group id), a resource-type checkbox grid sourced from the tenant's live resource types (becomes _type), narrowing filters (_elements, _typeFilter, _since as day/week/4-weeks presets or a custom RFC 3339 instant), and an optional job name. - Kick-offs and status polls are self-calls addressed via the request's Host header, with the caller's own Authorization and tenant forwarded -- the export runs with the user's credentials, so the workspace works under auth without a service account. Relative Content-Location poll URLs resolve against the same base. - /ui/bulk-export/active: one card per job (newest first) with status chips; in-progress cards htmx-refresh every 5s, each fetch performing exactly one server-side poll (202 records X-Progress, 200 stores the completion manifest's output files and stops the polling trigger). Complete cards list the files as download links with elapsed time; failed cards show the error verbatim with Retry (same parameters, fresh kick-off); Cancel DELETEs the job server-side. - Job state lives in the per-user settings document under the reserved byTenant subtree, like the import workspace. - The export form is parsed by hand (form_urlencoded): the repeated checkbox fields are more than serde_urlencoded accepts. Tests mount the UI over a mock FHIR export backend on a real socket so the Host-header self-calls loop back into the mock: kick-off query shape (_type/_elements/_since), patient and group paths, the 202-progress -> 200-manifest polling lifecycle, rejected kick-off -> failed card -> retry, and cancel reaching the API as a DELETE.
|
Screenshots of the workspace working end-to-end against a live server built from this branch (sqlite, 73 seeded resources) — real Export builder — named job, three resource types checked, Since = last 7 days: Active Exports, happy path — the job completed in 6s; the card stopped polling on its own and lists the completion manifest's file as a download link (only Patient had data among the checked types): Failure path — a Group-scope export against a group that doesn't exist: the card lands as Failed with the server's OperationOutcome verbatim and a Retry that re-runs the same parameters: Side observation while producing these, not this PR's concern: |
…lint CI's newer toolchain flags the must_use attribute async-trait 0.1.89 emits in its expansion (clippy::double_must_use, promoted by -D warnings) on every #[async_trait] trait, first surfacing in helios-observability's DashboardProvider. 0.1.92's expansion is clean. No source changes; the lock adds syn v3 alongside v2 as its dependency.
Same adjustment Import needed in #528: the Bulk Export workspace made the sidebar's Export item a real link, so the chrome spec expects a navLink instead of a soon-item. SQL-on-FHIR remains the section's only placeholder.
The Export and Import page headers stacked their action buttons under the title; page-head--row (the existing flex variant the resources workspace uses) puts them top-right, matching the design. The giant gap reported under the Export header turned out to be a Playwright full-page stitching artifact with the fixed nav rail, not a layout bug -- the screenshots are retaken at a tall viewport instead.
|
Follow-up on a layout question from review: the large empty band under the header in the earlier screenshots was a Playwright full-page-capture stitching artifact (the fixed nav rail confuses the scroll-and-stitch), not real layout — live DOM shows the standard 20px. What was real: the header action buttons stacked under the title. Fixed by using the existing page-head--row flex variant on the four bulk workspace pages, matching the design(actions top-right). Screenshots above re-captured artifact-free at a tall viewport. |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Resource-type picker isn't actually FHIR-version awareReviewing where the export form's resource-type checkboxes come from, I hit a discrepancy worth deciding on. Note this is pre-existing behavior this PR inherited, not something introduced here — but the Bulk Export page is a new consumer of it, so it seems like the right moment to flag it. The chain today
let resource_types = state
.compartments
.resource_type_names(&rt.id, helios_fhir::FhirVersion::default())
.await;→ The discrepancy The underlying data is version-specific, but two things flatten it before it reaches the page:
Net effect at runtime: the list reflects whatever the server seeded — The bundles do differ meaningfully:
Why it isn't visible in tests
Options
No objection to merging on this — just want the behavior to be a decision rather than an accident. |
# Conflicts: # crates/ui/Cargo.toml
…keys test The bulk-submit JWKS work brought in the pure-Rust rsa crate for RS384 public-JWK derivation, tripping the Security Audit on every PR: RUSTSEC-2023-0071 (Marvin Attack timing sidechannel) has no fixed release, and jwe.rs already rejects RSA-OAEP for exactly this reason. The JWKS handler's own doc promises an empty key set for non-ES384 algorithms -- the code now honors it. Both copies of the derivation (rest's derive_public_jwk, ui's signing_kid) return None for RS384 with the rationale in place; an RS384 deployment registers its key out-of-band, the assertion goes out without a kid, and the rsa crate leaves the lockfile. Also updates the UI keys test the redirect change left behind: the route permanently redirects to /.well-known/bulk-submit-jwks.json now, and the old 404/200 assertions were failing Test Rust on every PR's merge ref.
… only Steve's review of the export page traced the chain: the page passes FhirVersion::default() like the three existing callers, and the production HttpConformanceSource discards its version argument, so the list always reflects the server's seeded version (HFS_FHIR_VERSION) regardless of the sidebar selector. Correct on a single-version deployment; now documented at resource_type_names and the new call site so the behavior is a decision rather than an accident. Making the version real end-to-end across all four callers is filed separately.
|
Good catch, and agreed on the framing — making it a decision. Taken as option 1 now, option 2 filed:
Also resolved the conflicts: merged main (which brought #539 in) and carried the #560 fix onto this branch so the merge ref is green regardless of merge order — |
…ns idempotent Both fallouts of the root add-picker auto-opening on an empty document (#547): - axe can now see the value[x] choice selects that used to hide inside the collapsed details, and they never had an accessible name — each carries an aria-label built from the declarer and the pick-type message. - The editor-controls specs clicked the panel summary to open it, which now toggles an already-open picker closed and strands the fill behind an invisible filter. The page object gains openAddPanel() (click only when closed) and the value[x] spec checks the open attribute before toggling. Verified locally: the affected editor-controls and a11y editor specs pass against the rebuilt binary (9/9).
# Conflicts: # crates/ui/assets/app.css # locales/de/main.ftl # locales/en/main.ftl # locales/es/main.ftl
|
CI status: everything is green here except Test FHIRPath, and that one is infrastructure, not code — it has landed on The #564 target-GC can't rescue it either — it runs inside the job, after the checkout that never happens. It needs a manual prune of the agent's workspace (or pausing the agent so the job schedules elsewhere). Happy to re-trigger the job as soon as the disk has room; the rest of the run is already green (Linting, Test Rust, Test Python, Security Audit, Code Coverage). |



Closes #537
The pull-based companion to the Bulk Import workspace: a web UI over the server's own, already-shipped
$exportoperation, replacing the "Export" coming-soon placeholder in the Batch & Data nav. First functional round of the new Export design./ui/bulk-export_type. Nothing checked = every type._elements,_typeFilter, and_sinceas presets (last day / 7 days / 4 weeks / all time) or a custom RFC 3339 instant.Self-calls that work under auth
Kick-offs, status polls, and cancels target this same server via the request's Host header, forwarding the caller's own
Authorizationand tenant — the export runs with the user's credentials, no service token needed (unlike the conformance self-fetch, which still needsHFS_OUTBOUND_BEARER_TOKEN). RelativeContent-Locationpoll URLs resolve against the same base./ui/bulk-export/activeOne card per job, newest first, with status chips (in progress / complete / failed / cancelled):
X-Progressverbatim + Cancel (DELETEs the job server-side). Cards htmx-refresh every 5s; each fetch performs exactly one server-side poll, and the completed card simply lacks the trigger — the same no-JS polling pattern as the import workspace.Job state persists in the per-user settings document under the reserved
byTenantsubtree, like the import workspace.Tests
The suite mounts the UI over a mock FHIR export backend on a real socket, so the Host-header self-calls loop back into the mock: kick-off query shape asserted at the API (
_type/_elements/_since), Patient and Group paths, the 202-progress → 200-manifest lifecycle (polling starts and stops where it should), rejected kick-off → failed card → retry re-runs it, and cancel arriving as a DELETE. Full helios-ui suite green (incl. i18n parity ×3), clippy clean,--features uibuilds.Note: one implementation detail worth a look — the export form is parsed by hand via
form_urlencodedbecause the repeatedtypescheckboxes exceed whatserde_urlencodedaccepts.