diff --git a/airflow/www/static/js/dag/InstanceTooltip.test.tsx b/airflow/www/static/js/dag/InstanceTooltip.test.tsx index d2a4b92d38cc1..c52a465663665 100644 --- a/airflow/www/static/js/dag/InstanceTooltip.test.tsx +++ b/airflow/www/static/js/dag/InstanceTooltip.test.tsx @@ -40,12 +40,18 @@ describe("Test Task InstanceTooltip", () => { test("Displays a normal task", () => { const { getByText, queryByText } = render( , { wrapper: Wrapper } ); + expect(getByText("Trigger Rule: all_failed")).toBeDefined(); expect(getByText("Status: success")).toBeDefined(); expect(queryByText("Contains a note")).toBeNull(); expect(getByText("Duration: 00:00:00")).toBeDefined(); diff --git a/airflow/www/static/js/dag/InstanceTooltip.tsx b/airflow/www/static/js/dag/InstanceTooltip.tsx index f49b55a4fcdda..8fb021e30d9b7 100644 --- a/airflow/www/static/js/dag/InstanceTooltip.tsx +++ b/airflow/www/static/js/dag/InstanceTooltip.tsx @@ -83,6 +83,7 @@ const InstanceTooltip = ({ )} + {group.triggerRule && Trigger Rule: {group.triggerRule}} {note && Contains a note} ); diff --git a/airflow/www/static/js/dag/details/taskInstance/Details.tsx b/airflow/www/static/js/dag/details/taskInstance/Details.tsx index 942910879390e..3e3b9dc881459 100644 --- a/airflow/www/static/js/dag/details/taskInstance/Details.tsx +++ b/airflow/www/static/js/dag/details/taskInstance/Details.tsx @@ -42,7 +42,7 @@ const Details = ({ instance, group, dagId }: Props) => { const { taskId, runId, startDate, endDate, state, mappedStates, mapIndex } = instance; - const { isMapped, tooltip, operator, hasOutletDatasets } = group; + const { isMapped, tooltip, operator, hasOutletDatasets, triggerRule } = group; const { data: apiTI } = useTaskInstance({ dagId, @@ -168,6 +168,12 @@ const Details = ({ instance, group, dagId }: Props) => { {operator} )} + {triggerRule && ( + + Trigger Rule + {triggerRule} + + )} {startDate && ( diff --git a/airflow/www/static/js/types/index.ts b/airflow/www/static/js/types/index.ts index dc45f79460158..94e0e60526e18 100644 --- a/airflow/www/static/js/types/index.ts +++ b/airflow/www/static/js/types/index.ts @@ -17,7 +17,7 @@ * under the License. */ -import * as API from "./api-generated"; +import type * as API from "./api-generated"; type RunState = "success" | "running" | "queued" | "failed"; @@ -96,6 +96,7 @@ interface Task { isMapped?: boolean; operator?: string; hasOutletDatasets?: boolean; + triggerRule?: API.TriggerRule; } type RunOrdering = ( diff --git a/airflow/www/views.py b/airflow/www/views.py index 8b7e53f748c8d..ca8adac50bb6e 100644 --- a/airflow/www/views.py +++ b/airflow/www/views.py @@ -119,7 +119,7 @@ from airflow.utils.session import NEW_SESSION, create_session, provide_session from airflow.utils.state import State, TaskInstanceState from airflow.utils.strings import to_boolean -from airflow.utils.task_group import MappedTaskGroup, task_group_to_dict +from airflow.utils.task_group import MappedTaskGroup, TaskGroup, task_group_to_dict from airflow.utils.timezone import td_format, utcnow from airflow.version import version from airflow.www import auth, utils as wwwutils @@ -297,7 +297,7 @@ def dag_to_grid(dag: DagModel, dag_runs: Sequence[DagRun], session: Session): grouped_tis = {task_id: list(tis) for task_id, tis in itertools.groupby(query, key=lambda ti: ti.task_id)} def task_group_to_grid(item, grouped_tis, *, is_parent_mapped: bool): - if isinstance(item, AbstractOperator): + if not isinstance(item, TaskGroup): def _get_summary(task_instance): return { @@ -363,6 +363,7 @@ def set_overall_state(record): "is_mapped": isinstance(item, MappedOperator) or is_parent_mapped, "has_outlet_datasets": any(isinstance(i, Dataset) for i in (item.outlets or [])), "operator": item.operator_name, + "trigger_rule": item.trigger_rule, } # Task Group diff --git a/tests/www/views/test_views_grid.py b/tests/www/views/test_views_grid.py index 79e9401f684f0..171b48211f547 100644 --- a/tests/www/views/test_views_grid.py +++ b/tests/www/views/test_views_grid.py @@ -108,6 +108,7 @@ def test_no_runs(admin_client, dag_without_runs): "is_mapped": False, "label": "task1", "operator": "EmptyOperator", + "trigger_rule": "all_success", }, { "children": [ @@ -119,6 +120,7 @@ def test_no_runs(admin_client, dag_without_runs): "is_mapped": True, "label": "subtask2", "operator": "MockOperator", + "trigger_rule": "all_success", } ], "is_mapped": True, @@ -137,6 +139,7 @@ def test_no_runs(admin_client, dag_without_runs): "is_mapped": True, "label": "mapped", "operator": "MockOperator", + "trigger_rule": "all_success", } ], "id": "group", @@ -255,6 +258,7 @@ def test_one_run(admin_client, dag_with_runs: list[DagRun], session): "is_mapped": False, "label": "task1", "operator": "EmptyOperator", + "trigger_rule": "all_success", }, { "children": [ @@ -283,6 +287,7 @@ def test_one_run(admin_client, dag_with_runs: list[DagRun], session): "is_mapped": True, "label": "subtask2", "operator": "MockOperator", + "trigger_rule": "all_success", } ], "is_mapped": True, @@ -335,6 +340,7 @@ def test_one_run(admin_client, dag_with_runs: list[DagRun], session): "is_mapped": True, "label": "mapped", "operator": "MockOperator", + "trigger_rule": "all_success", }, ], "id": "group", @@ -398,6 +404,7 @@ def _expected_task_details(task_id, has_outlet_datasets): "is_mapped": False, "label": task_id, "operator": "EmptyOperator", + "trigger_rule": "all_success", } assert resp.status_code == 200, resp.json