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 4 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. |



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.