Pipeline export menu + Descriptions keyboard grid navigation¶
Motivation¶
Two researcher-facing gaps remained on the BLAST result page:
- #57 (roadmap R3) — the backend already renders Nextflow / Snakemake / CWL /
WDL modules (
GET /api/blast/jobs/{id}/export), but the SPA had no way to download them, so a researcher still could not slot a dashboard search into a pipeline without the CLI. - #30 — the Descriptions hits table (which can hold thousands of windowed rows) had no keyboard row navigation, so keyboard/screen-reader users could only Tab through every interactive cell.
User-facing change¶
- Pipeline export ▾ menu on the result header (next to Copy citation).
Picking Nextflow / Snakemake / CWL / WDL downloads the self-contained module
(
main.nf/Snakefile/blast_submit.cwl/blast_submit.wdl) that re-submits the job's exact parameters via onePOST /api/blast/jobscall. The download uses the server'sContent-Dispositionfilename; a job with no recorded parameters surfaces a clear "no recorded parameters to export" message (422) instead of a silent failure. - Keyboard navigation on the Descriptions hits table: Up/Down move the
focused row (roving
tabindex), Home jumps to the first row, End to the last loaded row. Pressing Down on the last painted row when more rows exist grows the #29 row window by one batch and moves focus into it — so keyboard users reach the whole result set without a mouse. - New Pipeline Export user-guide page documenting the end-to-end loop
(Export → pipeline runner →
POST /api/blast/jobs→ results in the same workspace), including theELB_BASE_URL/ELB_TOKEN/ELB_QUERY_FASTAruntime contract.
Implementation summary¶
web/src/api/blast.types.ts— newWorkflowExportFormatunion (mirrors the backendSUPPORTED_WORKFLOW_FORMATS).web/src/api/blast.ts—getWorkflowExport(jobId, format)(text download via the existingapi.getText, carrying the bearer; no SAS, no token in the file).web/src/pages/blastResults/workflowExportModel.ts(+ test) — pure format metadata +workflowExportFilenamefallback, with a guard test pinned to the backend_FORMAT_FILENAMES.web/src/pages/blastResults/BlastJobHeader.tsx—PipelineExportMenudropdown (click-outside / Esc close,role=menu),handleWorkflowExport(in-flight guard, 422-vs-transient error toasts), and a text-blob download helper.web/src/pages/blastResults/analytics/hitGridNav.ts(+ test) — purecomputeHitGridFocusreducer for Arrow/Home/End with the window-load seam, exhaustively unit-tested (empty set, first/last row, load seam, full walk with no skips/repeats, out-of-range clamp).web/src/pages/blastResults/analytics/BlastHitsTable.tsx— roving-tabindex rows,onKeyDownhandler (skips when focus is in a text input / textarea / select / contenteditable so it never hijacks a real control), focus-on-paint effect gated by a pending flag so unrelated re-renders never steal focus, and consistentaria-rowcount/aria-rowindex(header row = 1).docs/user-guide/pipeline-export.md+mkdocs.ymlnav entry.
The native <table> semantics are intentionally preserved (implicit
row/cell/columnheader/rowgroup roles) rather than overriding them with an
explicit role="grid", which would require re-roling every cell and risks an
assistive-technology regression. The interactive keyboard layer is purely
additive.
Design critique (self-critique rubric) + 5 hardening rounds¶
- Contract:
BlastHitsTable/BlastJobHeaderpublic props unchanged — no consumer (DescriptionsTabBody, BlastResults) needed edits. NewWorkflowExportFormattype andgetWorkflowExportmethod are additive. - Liveness/loops: the window-grow on ArrowDown is bounded by
Math.min(current + ROW_STEP, hits.length); ArrowDown on the true last row is a no-op. No unbounded loop. - Idempotency/concurrency:
handleWorkflowExportearly-returns while an export is in flight and menu items are disabled, so a double-click cannot fire two downloads. - Partial failure: export errors degrade to a toast (422 = permanent "no parameters", else transient) — never an unhandled rejection.
- Security: the export route is
require_caller-protected; the download carries the bearer viaapi.getTextand the rendered module embeds no SAS / token / storage URL. - Backward-compat: all additions are optional/additive; no field removed or renamed.
Hardening rounds applied: (1) consistent header aria-rowindex=1; (2) grid
nav skips arrow-consuming form controls; (3) 422-vs-transient export error
messages; (4) reset the pending-focus flag when the hit set changes; (5) no-op
key presses consume the key (no page scroll) without state churn.
Remaining gate (#30)¶
Keyboard navigation + the windowing seam are implemented and unit-tested, but a screen-reader pass (NVDA / VoiceOver) to confirm the row announcements and the load-more focus move read correctly is still required before #30 closes — that verification cannot run in a code session, so #30 stays open for it.
Validation¶
cd web && npx vitest run— 945 passed (16 new:workflowExportModel5,hitGridNav11).cd web && npm run build— clean (tsc + vite).cd web && npx eslint <touched>— clean.uv run pytest -q api/tests/test_blast_citation.py api/tests/test_blast_workflow_export.py— 29 passed (backends unchanged).