Skip to content

UI: Use react-query native error state for bulk action hooks#67284

Draft
pierrejeambrun wants to merge 8 commits into
apache:mainfrom
astronomer:fix/bulk-action-error-handling
Draft

UI: Use react-query native error state for bulk action hooks#67284
pierrejeambrun wants to merge 8 commits into
apache:mainfrom
astronomer:fix/bulk-action-error-handling

Conversation

@pierrejeambrun
Copy link
Copy Markdown
Member

Stacked on #67095. The diff here includes the 7 commits from #67095 — the only commit relevant to this PR is the top one, UI: Use react-query native error state for bulk action hooks. Once #67095 merges, GitHub will auto-clean the diff down to that single commit.

useBulkTaskInstances and useBulkDeleteDagRuns both kept the same anti-pattern @bbovenzi flagged on #67095: a useState<unknown> for the error, an onError callback that just forwarded the network error to setError, and a tiny helper that grabbed errors[0] from the response body and re-shaped it into {body:{detail:...}} so ErrorAlert could render the first per-entity failure. That hid every error past the first and duplicated mutation state into React state for no reason.

Both hooks now return { bulkAction, data, error, isPending, reset } straight from useMutation:

  • error covers HTTP-level failures (4xx/5xx, network).
  • data.delete.errors / data.update.errors carries the per-entity failures the backend returns on a 200 response (partial success). Consumers render every entry, not just the first.
  • reset replaces the consumer-side setError(undefined) calls.

onSuccess still invalidates queries unconditionally, fires the toaster + clears selection when success.length > 0, and only closes the dialog when errors.length === 0 — partial-success keeps the dialog open so the user can read what failed.


Was generative AI tooling used to co-author this PR?
  • Yes — Claude Code (Opus 4.7)

Generated-by: Claude Code (Opus 4.7) following the guidelines

Restores feature parity with Airflow 2.x where DagRunModelView exposed
collective Delete on the Dag Runs list view. Adds:

- PATCH /dags/{dag_id}/dagRuns — bulk endpoint structured like the
  existing bulk task-instances endpoint. Only ``delete`` is supported in
  this PR; ``create`` and ``update`` are wired to return 405 in the
  BulkResponse so future PRs can fill them in without changing the
  route surface.
- BulkDagRunService with deletable-state enforcement (matches the
  single-run delete: only QUEUED / SUCCESS / FAILED can be deleted),
  per-Dag authorization caching for the wildcard path
  /dags/~/dagRuns, and ``action_on_non_existence: fail | skip``
  semantics.
- Row selection + a Delete bulk action on the runs list page,
  mirroring how Task Instances does it.

Bulk Mark-as and Bulk Clear are intentionally out of scope and will
follow in separate PRs.

The grid view stays single-select; multi-select on the grid was not
available in 2.x either, and the runs list page is the natural target
for bulk operations on a filtered set (e.g. state=failed).

closes: apache#52439
- Switch BulkDagRunService._fetch_dag_runs to tuple_(dag_id, run_id).in_()
  to avoid a Cartesian over-fetch when /dags/~/dagRuns is called with
  pairs spanning multiple Dags. Matches BulkTaskInstanceService.
- Narrow _check_dag_authorization's method type to Literal["DELETE"];
  this PR only does delete, no point in exposing PUT/POST/GET on the
  signature.
- Add a wildcard test that exercises the per-Dag authorization path
  (limited user accepted for one Dag, rejected with 403 in the
  BulkResponse for a team-restricted Dag).
Pulls in the substantive UI feedback @bbovenzi left on apache#66554 so this
PR lands without re-litigating the same comments. The closed PR was
broader (clear / mark / delete) but the structural feedback applies
verbatim to delete-only:

- Move all DagRuns-related files into ``pages/DagRuns/``, mirroring
  ``pages/TaskInstances/``. Adds a small ``index.ts`` re-export so
  ``router.tsx`` keeps the same import path.
- Rename ``useBulkDagRuns`` to ``useBulkDeleteDagRuns`` so the hook
  name matches the single button it serves (one-hook-one-button
  symmetry — when we add bulk update/clear we'll add sibling hooks).
- Stop hand-rolling pending/error state with ``useState<unknown>``;
  read ``error`` straight off ``useMutation``'s return.
- Surface ALL per-entity errors from ``BulkResponse.delete.errors``
  instead of just the first; render each as its own ``Alert`` row.
- Only invalidate the dag-runs / task-instances queries when at least
  one entry actually succeeded — a 200 with all-errors should not
  churn the table.
- Keep the dialog open when per-entity errors come back so the user
  can read what failed; ``reset()`` clears them on close.
Backend:
- Inline ``_resolve_dag_id``, ``_result_key`` and ``_fetch_dag_runs`` —
  each had exactly one caller. Memory rule added so future PRs avoid
  premature helpers.
- Drop the deletable-state restriction. Bulk task-instance delete has no
  such restriction; bulk Dag-run delete shouldn't either.
- Emit one 404 error per missing entity when ``action_on_non_existence``
  is ``fail`` instead of collating them into a single error, and stop
  early-returning so matched runs still get deleted. The invariant
  ``len(success) + len(errors) == len(requested entities)`` now holds.
- Distinguish the two ways a wildcard can leak into ``dag_id`` (path
  vs body) in the 400 message, mirroring
  ``BulkTaskInstanceService._categorize_entities``.

UI:
- Mirror ``useBulkTaskInstances`` exactly: bring back ``useState`` for
  the error, the shared ``handleActionResult`` helper, single-error
  surfacing via ``ErrorAlert``, and the ``bulkAction(requestBody)``
  shape so the consumer constructs the full ``BulkBody``. Brent's prior
  review of the closed twin PR pushed past this pattern, but until TI
  is updated we want both hooks symmetrical — a follow-up can improve
  both at once.
- Inline the affected-runs column array into ``BulkDeleteDagRunsButton``
  and delete the standalone ``bulkDagRunsColumns.tsx`` file (single
  caller).
Adds ``requires_access_dag_run_bulk`` in ``core_api/security.py``
following the ``requires_access_connection_bulk`` pattern. The
dependency reads the parsed ``BulkBody[BulkDAGRunBody]``, resolves
each entity's ``dag_id`` (body wins, falling back to the path),
collects team mappings per Dag, and uses ``batch_is_authorized_dag``
to enforce auth before the route handler runs.

The route now declares only this dependency plus ``action_logging``;
the per-entity auth check is no longer duplicated inside
``BulkDagRunService``. Unauthorized requests fail with a single
route-level 403 instead of returning 200 with per-entity 403s in the
``BulkResponse``, matching how connections / pools / variables behave.
Single ``DELETE /dags/{dag_id}/dagRuns/{dag_run_id}`` rejects RUNNING
runs with 409; bulk delete now does the same — a RUNNING entity yields
a per-entity 409 in ``BulkResponse.errors`` and the matched non-running
entities still get deleted.

Also renames ``DAGRunPatchStates`` to ``DagRunMutableStates`` since it
now gates both PATCH (mark-as) and DELETE — same set of states (QUEUED,
SUCCESS, FAILED), broader meaning. Propagated through the route handlers,
the bulk service, and the UI components that import the generated type.
Bulk delete only invalidated the top-level dag-runs and task-instances
lists. Two more cache sets stay stale otherwise:

- Per-attempt TI caches (log / extra links / try details), keyed by
  TI identity. When the user navigates back to a TI try detail via
  the browser back button after deleting its run, react-query serves
  the cached log instead of letting the request hit and return 404.

- The grid view query set for each affected Dag — the grid renders
  one bar per Dag Run, so bulk delete literally removes bars.

The per-attempt set mirrors the addition ``useBulkTaskInstances``
already gained in apache#67212. The grid invalidation is specific to this
hook because deleting Dag Runs (unlike deleting TIs) changes what the
grid bars themselves represent.

The affected ``dag_id`` set is captured in ``bulkAction`` from the
request body and read in ``onSuccess``, same lifecycle as
``useBulkClearTaskInstances``'s ``byDagRun`` grouping.
Both ``useBulkTaskInstances`` and ``useBulkDeleteDagRuns`` mirrored
the same anti-pattern: a ``useState<unknown>`` field, an ``onError``
callback that just forwarded to ``setError``, and a tiny helper that
grabbed ``errors[0]`` from the response body and re-shaped it into
``{body:{detail:...}}`` so ``ErrorAlert`` could render the first
per-entity failure. That hid every error past the first and
duplicated mutation state into React state for no reason.

Both hooks now return ``{ bulkAction, data, error, isPending, reset }``
straight from ``useMutation``:

- ``error`` covers HTTP-level failures (4xx/5xx, network).
- ``data.delete.errors`` / ``data.update.errors`` carries the
  per-entity failures the backend returns on a 200 response (partial
  success). Consumers render every entry, not just the first.
- ``reset`` replaces the consumer-side ``setError(undefined)`` calls.

``onSuccess`` still invalidates queries unconditionally, fires the
toaster + clears selection when ``success.length > 0``, and only
closes the dialog when ``errors.length === 0`` — partial-success
keeps the dialog open so the user can read what failed.

The three consumers (``BulkMarkTaskInstancesAsButton``,
``BulkDeleteTaskInstancesButton``, ``BulkDeleteDagRunsButton``) now
render network errors via ``ErrorAlert`` and per-entity errors as a
``Stack`` of ``Alert`` rows below it.
@boring-cyborg boring-cyborg Bot added area:airflow-ctl area:API Airflow's REST/HTTP API area:UI Related to UI/UX. For Frontend Developers. backport-to-airflow-ctl/v0-1-test labels May 21, 2026
@pierrejeambrun pierrejeambrun mentioned this pull request May 21, 2026
1 task
@pierrejeambrun
Copy link
Copy Markdown
Member Author

Only the last commit is relevant.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:airflow-ctl area:API Airflow's REST/HTTP API area:UI Related to UI/UX. For Frontend Developers. backport-to-airflow-ctl/v0-1-test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant