Skip to content

Commit d7922e8

Browse files
committed
Show current task executions in the UI
A loop pass or a mapped expansion reuses a task id and map index, so the UI cannot identify an execution by (task_id, map_index): the logs, XCom, state or HITL page opened for one pass could show another's. Pages and mutations therefore carry the region id and index, and row selection is keyed by attempt UUID so two executions that share public coordinates stay distinct even when they are selected together. The run-level Execution tab lists live task instances grouped by loop iteration and mapped expansion, because that is how a user thinks about what is running. Clearing a selection goes by UUID through a dialog that offers the later-iteration and whole-expansion choices the clear API takes, so a user can decide how far a clear reaches. Gantt segments link to the exact region and try, and a retired try opens only when both are given, since a try number alone is ambiguous. Only the current view is covered. Selecting history by clear boundary is deliberately absent, so no trajectory parameter is read or written.
1 parent 4a85521 commit d7922e8

119 files changed

Lines changed: 4169 additions & 875 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎airflow-core/src/airflow/api_fastapi/core_api/datamodels/task_instances.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ class TaskInstanceResponse(OmitsMissingRegion, BaseModel):
6060
map_index: int
6161
region_id: RegionId = None
6262
region_index: RegionIndex = None
63+
in_loop: bool
6364
logical_date: datetime | None
6465
run_after: datetime
6566
start_date: datetime | None

‎airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5250,6 +5250,9 @@ components:
52505250
region_index:
52515251
type: integer
52525252
title: Region Index
5253+
in_loop:
5254+
type: boolean
5255+
title: In Loop
52535256
logical_date:
52545257
anyOf:
52555258
- type: string
@@ -5401,6 +5404,7 @@ components:
54015404
- dag_id
54025405
- dag_run_id
54035406
- map_index
5407+
- in_loop
54045408
- logical_date
54055409
- run_after
54065410
- start_date

‎airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17896,6 +17896,9 @@ components:
1789617896
region_index:
1789717897
type: integer
1789817898
title: Region Index
17899+
in_loop:
17900+
type: boolean
17901+
title: In Loop
1789917902
logical_date:
1790017903
anyOf:
1790117904
- type: string
@@ -18047,6 +18050,7 @@ components:
1804718050
- dag_id
1804818051
- dag_run_id
1804918052
- map_index
18053+
- in_loop
1805018054
- logical_date
1805118055
- run_after
1805218056
- start_date

‎airflow-core/src/airflow/api_fastapi/core_api/services/public/task_coordinates.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ def __getattr__(self, name: str) -> Any:
5353
if self.projected_map_index is not None:
5454
return self.projected_map_index
5555
return self.resolver.public_map_index(self.value)
56+
if name == "in_loop":
57+
return self.value.region_id != SENTINEL_REGION_ID and self.resolver._is_in_loop(self.value)
5658
return getattr(self.value, name)
5759

5860

‎airflow-core/src/airflow/models/task_coordinates.py‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ class TaskCoordinateResolver:
164164
_dags: dict[UUID, SerializedDAG] = attrs.field(factory=dict, init=False)
165165
_region_nodes: dict[UUID, str | None] = attrs.field(factory=dict, init=False)
166166
_regional_tasks: dict[tuple[str, str | None, str], bool] = attrs.field(factory=dict, init=False)
167+
_looped_tasks: dict[tuple[UUID, str], bool] = attrs.field(factory=dict, init=False)
167168

168169
@classmethod
169170
def for_dag(cls, dag: SerializedDAG | None, session: Session) -> TaskCoordinateResolver:
@@ -201,7 +202,16 @@ def get_task(
201202
raise ValueError(f"Pinned Dag for {dag_id}/{run_id} not found")
202203
return dag.get_task(task_id)
203204

204-
def find_task(self, ti: TaskInstance) -> SerializedOperator | None:
205+
def _is_in_loop(self, ti: TaskCoordinate) -> bool:
206+
if ti.dag_version_id is None:
207+
return False
208+
key = (ti.dag_version_id, ti.task_id)
209+
if key not in self._looped_tasks:
210+
task = self.find_task(ti)
211+
self._looped_tasks[key] = task is not None and enclosing_loop(task) is not None
212+
return self._looped_tasks[key]
213+
214+
def find_task(self, ti: TaskCoordinate) -> SerializedOperator | None:
205215
"""Return the task of ``ti`` from its pinned Dag, or None when that Dag or the task is gone."""
206216
if ti.dag_version_id is None or (dag := self.get_dag(ti.dag_version_id)) is None:
207217
return None

‎airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8067,6 +8067,10 @@ export const $TaskInstanceResponse = {
80678067
type: 'integer',
80688068
title: 'Region Index'
80698069
},
8070+
in_loop: {
8071+
type: 'boolean',
8072+
title: 'In Loop'
8073+
},
80708074
logical_date: {
80718075
anyOf: [
80728076
{
@@ -8351,7 +8355,7 @@ export const $TaskInstanceResponse = {
83518355
}
83528356
},
83538357
type: 'object',
8354-
required: ['id', 'task_id', 'dag_id', 'dag_run_id', 'map_index', 'logical_date', 'run_after', 'start_date', 'end_date', 'duration', 'state', 'try_number', 'max_tries', 'task_display_name', 'dag_display_name', 'hostname', 'unixname', 'pool', 'pool_slots', 'queue', 'priority_weight', 'operator', 'operator_name', 'queued_when', 'scheduled_when', 'pid', 'executor', 'executor_config', 'note', 'rendered_map_index', 'trigger', 'triggerer_job', 'dag_version'],
8358+
required: ['id', 'task_id', 'dag_id', 'dag_run_id', 'map_index', 'in_loop', 'logical_date', 'run_after', 'start_date', 'end_date', 'duration', 'state', 'try_number', 'max_tries', 'task_display_name', 'dag_display_name', 'hostname', 'unixname', 'pool', 'pool_slots', 'queue', 'priority_weight', 'operator', 'operator_name', 'queued_when', 'scheduled_when', 'pid', 'executor', 'executor_config', 'note', 'rendered_map_index', 'trigger', 'triggerer_job', 'dag_version'],
83558359
title: 'TaskInstanceResponse',
83568360
description: 'TaskInstance serializer for responses.'
83578361
} as const;

‎airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2199,6 +2199,7 @@ export type TaskInstanceResponse = {
21992199
map_index: number;
22002200
region_id?: string;
22012201
region_index?: number;
2202+
in_loop: boolean;
22022203
logical_date: string | null;
22032204
run_after: string;
22042205
start_date: string | null;

‎airflow-core/src/airflow/ui/public/i18n/locales/en/common.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,7 @@
466466
"executorConfig": "Executor Config",
467467
"hostname": "Hostname",
468468
"id": "ID",
469+
"iteration": "Iteration",
469470
"maxTries": "Max Tries",
470471
"pid": "PID",
471472
"pool": "Pool",

‎airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,17 @@
6868
"upcomingCount_one": "{{count}} Upcoming Deadline",
6969
"upcomingCount_other": "{{count}} Upcoming Deadlines"
7070
},
71+
"execution": {
72+
"clearSelected": "Clear selected executions",
73+
"empty": "No executions in this run.",
74+
"iteration": "{{node}} · Iteration {{index}}",
75+
"mapped_one": "{{task}} ({{count}} mapped task on this page)",
76+
"mapped_other": "{{task}} ({{count}} mapped tasks on this page)",
77+
"page": "Showing {{start}}–{{end}} of {{total}} executions",
78+
"runTasks": "Run tasks",
79+
"select": "Select {{task}}",
80+
"title": "Execution"
81+
},
7182
"extraLinks": "Extra Links",
7283
"grid": {
7384
"buttons": {

‎airflow-core/src/airflow/ui/public/i18n/locales/en/dags.json‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,15 @@
9292
"existingTasks": "Clear existing tasks",
9393
"future": "Future",
9494
"keepTaskState": "Keep task state and resume",
95+
"laterIterations": "Later iterations",
9596
"onlyFailed": "Clear only failed tasks",
9697
"past": "Past",
9798
"preventRunningTasks": "Prevent rerun if task is running",
9899
"queueNew": "Queue up new tasks",
99100
"runOnLatestVersion": "Run with latest bundle version",
100101
"runOnLatestVersionForced": "Always uses the latest — there's no earlier version to go back to",
101-
"upstream": "Upstream"
102+
"upstream": "Upstream",
103+
"wholeExpansion": "Whole expansion"
102104
}
103105
},
104106
"runStateCounts": {

0 commit comments

Comments
 (0)