Skip to content

Conversation

@arjav1528
Copy link
Contributor

@arjav1528 arjav1528 commented Jan 6, 2026

Description

This PR adds two new React components that restore the task group marking functionality:

  1. MarkGroupTaskInstanceAsButton - A dropdown button component that allows users to select a state (success/failed) and opens a dialog for confirmation
  2. MarkGroupTaskInstanceAsDialog - A dialog component that provides:
    • Options to include past/future/upstream/downstream task instances
    • Dry-run preview showing which task instances will be affected
    • Ability to add notes to the state change
    • Bulk update using the task instance bulk API for efficiency

The button is integrated into the Group Task Instance page header, alongside the existing Clear button.

Screenshots

image image

Solves #60121

@boring-cyborg boring-cyborg bot added the area:UI Related to UI/UX. For Frontend Developers. label Jan 6, 2026
@bohdan-pd
Copy link

@arjav1528 Thank you for working on that! 👍

@arjav1528 arjav1528 force-pushed the main branch 2 times, most recently from 5db3cd8 to c9d177e Compare January 6, 2026 18:50
@arjav1528 arjav1528 marked this pull request as draft January 6, 2026 18:50
@arjav1528 arjav1528 marked this pull request as ready for review January 6, 2026 18:50
@arjav1528
Copy link
Contributor Author

@arjav1528 Thank you for working on that! 👍

is the UI ok or do you want me to refactor that

Copy link
Contributor

@bbovenzi bbovenzi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for taking this on! Do you mind doing more manually testing to find where MarkAs isn't quite working? I think there are some issues with the API that we need to tackle before we can merge any UI changes.

@arjav1528 arjav1528 force-pushed the main branch 2 times, most recently from b068b63 to da03292 Compare January 7, 2026 16:39
Copy link
Contributor

@bbovenzi bbovenzi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried using this locally and still had issues. Could you both manually test this out and also add fastapi test for dry runs?

@arjav1528
Copy link
Contributor Author

arjav1528 commented Jan 7, 2026

I tried using this locally and still had issues. Could you both manually test this out and also add fastapi test for dry runs?

Sorry about the trouble. I’ve tested this locally and couldn’t reproduce the issue. I have reproduced the issue, solved it, you can run it locally now

@arjav1528 arjav1528 force-pushed the main branch 3 times, most recently from d24fdae to 34153d8 Compare January 8, 2026 13:12
@arjav1528 arjav1528 requested a review from choo121600 as a code owner January 8, 2026 13:12
@arjav1528
Copy link
Contributor Author

I tried using this locally and still had issues. Could you both manually test this out and also add fastapi test for dry runs?

Sorry about the trouble. I’ve tested this locally and couldn’t reproduce the issue. I have reproduced the issue, solved it, you can run it locally now

@bbovenzi I am not sure why is this test failing, coz I havent made any changes related to this

Copy link
Contributor

@bbovenzi bbovenzi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking much better! Just a few more comments

Copy link
Member

@pierrejeambrun pierrejeambrun left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for that pull request. People have been asking for this feature back a lot! 🎉

For the two reasons mentioned bellow (crafting the payload requires all the TIs of the group which isn't easy to get on the client side because of pagination + the return type of the API will create confusion).

I think the best approach is to create a couple of endpoint specifically for patching a TI group. (dry_run and non dry_run version). Refactoring and reusing the code of the patch ti endpoint, because it already returns a TaskInstanceCollectionResponse (for the scenario of patching all mapped index at once).

This should make this PR much simpler. (basically just a simple call to patch, with the group id + payload, and apply the change to all tasks within the group)

Comment on lines 62 to 72
const { data: groupTaskInstances } = useTaskInstanceServiceGetTaskInstances(
{
dagId,
dagRunId: runId,
taskGroupId: groupId,
},
undefined,
{
enabled: open,
},
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For instance by doing this, you only retrieve the first page, the 50 first TI of the group, not all the TIs, so you could be missing some.

@arjav1528
Copy link
Contributor Author

arjav1528 commented Jan 9, 2026

@bbovenzi are there any changes left?, would love to implement them
could you please merge this PR at your availability

Copy link
Member

@jason810496 jason810496 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Thanks for the PR!

Comment on lines 1050 to 1054
if not group_tis:
raise HTTPException(
status.HTTP_404_NOT_FOUND,
f"No task instances found for task group '{task_group_id}' in DAG '{dag_id}' and run '{dag_run_id}'",
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be better to move the if not group_tis: raise ... case handling to _get_task_group_task_instances as well?

Comment on lines 1068 to 1070
# Collect all affected task instances (including upstream/downstream/future/past)
all_affected_tis: list[TI] = []
seen_ti_keys: set[tuple[str, str, str, int]] = set()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems we could consolidate all_affected_tis: list and seen_ti_keys: set as a dict.

Comment on lines 1173 to 1217
# Update state and get all affected TIs (including upstream/downstream/future/past)
updated_tis = dag.set_task_instance_state(
task_id=ti.task_id,
run_id=dag_run_id,
map_indexes=map_indexes,
state=data["new_state"],
upstream=body.include_upstream or False,
downstream=body.include_downstream or False,
future=body.include_future or False,
past=body.include_past or False,
commit=True,
session=session,
)

if not updated_tis:
raise HTTPException(
status.HTTP_409_CONFLICT,
f"Task id {ti.task_id} is already in {data['new_state']} state",
)

# Track unique affected TIs and trigger listeners
for updated_ti in updated_tis:
ti_key = (
updated_ti.dag_id,
updated_ti.run_id,
updated_ti.task_id,
updated_ti.map_index if updated_ti.map_index is not None else -1,
)
if ti_key not in seen_ti_keys:
seen_ti_keys.add(ti_key)
all_affected_tis.append(updated_ti)

# Trigger listeners
try:
if data["new_state"] == TaskInstanceState.SUCCESS:
get_listener_manager().hook.on_task_instance_success(
previous_state=None, task_instance=updated_ti
)
elif data["new_state"] == TaskInstanceState.FAILED:
get_listener_manager().hook.on_task_instance_failed(
previous_state=None,
task_instance=updated_ti,
error=f"TaskInstance's state was manually set to `{TaskInstanceState.FAILED}`.",
)
except Exception:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use _patch_task_instance_state helper here I think.

def _patch_task_instance_state(
task_id: str,

Comment on lines 1167 to 1170
# Process each task in the group
for ti in group_tis:
# Update state if requested
if data.get("new_state"):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the check be

    if data.get("new_state"):
        # For each task in the group, simulate state change
        for ti in group_tis:

here? So that we can skip whole the loop if we don't update the state.

Comment on lines 1234 to 1255
# Refresh the affected TIs from the database to get the latest state and notes
ti_keys_list = list(seen_ti_keys)
refreshed_tis = session.scalars(
select(TI)
.where(tuple_(TI.dag_id, TI.run_id, TI.task_id, TI.map_index).in_(ti_keys_list))
.options(joinedload(TI.rendered_task_instance_fields))
).all()
all_affected_tis = list(refreshed_tis)

# Also include group TIs that had notes updated but weren't in the state change list
# (to avoid duplicates, only add TIs not already in seen_ti_keys)
if data.get("note") is not None:
for ti in group_tis:
ti_key = (
ti.dag_id,
ti.run_id,
ti.task_id,
ti.map_index if ti.map_index is not None else -1,
)
if ti_key not in seen_ti_keys:
seen_ti_keys.add(ti_key)
all_affected_tis.append(ti)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, we are able to consolidate as one dict instead of having list and set.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jason810496 I have applied all the changes, do review them, and merge as per your availability after the CI checks

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

Labels

area:UI Related to UI/UX. For Frontend Developers.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants