This is an automated email from the ASF dual-hosted git repository.

pierrejeambrun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 0619574dd21 Add `has_note` key to the API response for TI to render 
saved note indicator in Airflow 3 Grid View UI (#68979)
0619574dd21 is described below

commit 0619574dd215787b18ef0e760f54d891eb9b42ae
Author: Jayachandra Kasarla <[email protected]>
AuthorDate: Fri Jun 26 15:20:29 2026 +0530

    Add `has_note` key to the API response for TI to render saved note 
indicator in Airflow 3 Grid View UI (#68979)
    
    * Added has_note key
    
    * Added tests for has_note
    
    * Modified GridTi.tsx to render saved note indicator
    
    * format
    
    * added has_note to schemas.gen.ts
    
    ---------
    
    Co-authored-by: Jayachandra Kasarla <[email protected]>
---
 .../api_fastapi/core_api/datamodels/ui/grid.py     |  1 +
 .../api_fastapi/core_api/openapi/_private_ui.yaml  |  4 +++
 .../airflow/api_fastapi/core_api/routes/ui/grid.py | 11 ++++++-
 .../api_fastapi/core_api/services/ui/grid.py       |  5 ++++
 .../airflow/ui/openapi-gen/requests/schemas.gen.ts |  5 ++++
 .../airflow/ui/openapi-gen/requests/types.gen.ts   |  1 +
 .../airflow/ui/src/layouts/Details/Grid/GridTI.tsx | 16 +++++++++-
 .../layouts/Details/Grid/TaskInstancesColumn.tsx   |  1 +
 .../api_fastapi/core_api/routes/ui/test_grid.py    | 35 +++++++++++++++++++++-
 9 files changed, 76 insertions(+), 3 deletions(-)

diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/grid.py 
b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/grid.py
index 19278430d3f..98adbb537c6 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/grid.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/grid.py
@@ -34,6 +34,7 @@ class LightGridTaskInstanceSummary(BaseModel):
     min_start_date: datetime | None
     max_end_date: datetime | None
     dag_version_number: int | None = None
+    has_note: bool = False
 
 
 class GridTISummaries(BaseModel):
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml 
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
index d786e259b4e..5d51f7b7480 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
+++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
@@ -3128,6 +3128,10 @@ components:
           - type: integer
           - type: 'null'
           title: Dag Version Number
+        has_note:
+          type: boolean
+          title: Has Note
+          default: false
       type: object
       required:
       - task_id
diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/grid.py 
b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/grid.py
index 72d92fbef2c..ea9de387c39 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/grid.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/grid.py
@@ -68,7 +68,7 @@ from airflow.models.dag_version import DagVersion
 from airflow.models.dagrun import DagRun
 from airflow.models.deadline import Deadline
 from airflow.models.serialized_dag import SerializedDagModel
-from airflow.models.taskinstance import TaskInstance
+from airflow.models.taskinstance import TaskInstance, TaskInstanceNote
 from airflow.utils.session import create_session
 
 if TYPE_CHECKING:
@@ -373,6 +373,7 @@ def _build_ti_summaries(
             start_date=ti.start_date,
             end_date=ti.end_date,
             dag_version_number=getattr(ti, "version_number", None),
+            has_note=bool(getattr(ti, "has_note", False)),
         )
     if not ti_details:
         return None
@@ -467,6 +468,13 @@ def get_grid_ti_summaries_stream(
         # database connection open for the entire stream duration.
         # See https://github.com/apache/airflow/issues/65010.
 
+        has_note_subq = (
+            exists()
+            .where(TaskInstanceNote.ti_id == TaskInstance.id)
+            .correlate(TaskInstance)
+            .label("has_note")
+        )
+
         for run_id in run_ids or []:
             with create_session(scoped=False) as session:
                 tis = session.execute(
@@ -477,6 +485,7 @@ def get_grid_ti_summaries_stream(
                         TaskInstance.start_date,
                         TaskInstance.end_date,
                         DagVersion.version_number,
+                        has_note_subq,
                     )
                     .outerjoin(DagVersion, TaskInstance.dag_version_id == 
DagVersion.id)
                     .where(TaskInstance.dag_id == dag_id)
diff --git a/airflow-core/src/airflow/api_fastapi/core_api/services/ui/grid.py 
b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/grid.py
index bff2fd66296..a40a6f726e2 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/services/ui/grid.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/grid.py
@@ -43,6 +43,7 @@ class GridNodeAgg:
     min_start_date: datetime | None = None
     max_end_date: datetime | None = None
     dag_version_number: int | None = None
+    has_note: bool = False
 
     def add_ti(
         self,
@@ -51,6 +52,7 @@ class GridNodeAgg:
         start_date: datetime | None,
         end_date: datetime | None,
         dag_version_number: int | None,
+        has_note: bool = False,
     ) -> None:
         """Merge one task instance row into the summary."""
         self.child_states[state] += 1
@@ -62,6 +64,7 @@ class GridNodeAgg:
             self.dag_version_number is None or dag_version_number > 
self.dag_version_number
         ):
             self.dag_version_number = dag_version_number
+        self.has_note = self.has_note or has_note
 
     def merge(self, other: GridNodeAgg) -> None:
         """Merge another summary into this one."""
@@ -78,6 +81,7 @@ class GridNodeAgg:
             self.dag_version_number is None or other.dag_version_number > 
self.dag_version_number
         ):
             self.dag_version_number = other.dag_version_number
+        self.has_note = self.has_note or other.has_note
 
     def with_placeholder_state(self) -> GridNodeAgg:
         """Represent mapped tasks without rows as a single no-status square in 
the grid."""
@@ -133,6 +137,7 @@ def _get_aggs_for_node(summary: GridNodeAgg) -> dict[str, 
Any]:
         "max_end_date": summary.max_end_date,
         "child_states": _serialize_child_states(summary.child_states),
         "dag_version_number": summary.dag_version_number,
+        "has_note": summary.has_note,
     }
 
 
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts 
b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
index 9685fd6b3cb..0f98b36e8c7 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
@@ -9737,6 +9737,11 @@ export const $LightGridTaskInstanceSummary = {
                 }
             ],
             title: 'Dag Version Number'
+        },
+        has_note: {
+            type: 'boolean',
+            title: 'Has Note',
+            default: false
         }
     },
     type: 'object',
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts 
b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
index 7bd02c55979..81b5665a333 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
@@ -2472,6 +2472,7 @@ export type LightGridTaskInstanceSummary = {
     min_start_date: string | null;
     max_end_date: string | null;
     dag_version_number?: number | null;
+    has_note?: boolean;
 };
 
 /**
diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.tsx
index 6cd21d252f9..4ced7ca10c1 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.tsx
@@ -25,8 +25,12 @@ import TaskInstanceTooltip from 
"src/components/TaskInstanceTooltip";
 import { useHover } from "src/context/hover";
 import { buildTaskInstanceUrl } from "src/utils/links";
 
+const NOTE_GRADIENT =
+  "linear-gradient(45deg, var(--chakra-colors-color-palette-solid) 65%, 
var(--chakra-colors-color-palette-emphasized) 65%)";
+
 type Props = {
   readonly dagId: string;
+  readonly hasNote?: boolean;
   readonly instance: LightGridTaskInstanceSummary;
   readonly isGroup?: boolean;
   readonly isMapped?: boolean | null;
@@ -36,7 +40,16 @@ type Props = {
   readonly taskId: string;
 };
 
-export const GridTI = ({ dagId, instance, isGroup, isMapped, onClick, runId, 
taskId }: Props) => {
+export const GridTI = ({
+  dagId,
+  hasNote = false,
+  instance,
+  isGroup,
+  isMapped,
+  onClick,
+  runId,
+  taskId,
+}: Props) => {
   const { hoveredTaskId, setHoveredTaskId } = useHover();
   const { groupId: selectedGroupId, taskId: selectedTaskId } = useParams();
   const location = useLocation();
@@ -107,6 +120,7 @@ export const GridTI = ({ dagId, instance, isGroup, 
isMapped, onClick, runId, tas
               justifyContent="center"
               minH={0}
               p={0}
+              style={hasNote ? { background: NOTE_GRADIENT } : undefined}
               variant="solid"
               width="14px"
             >
diff --git 
a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/TaskInstancesColumn.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/TaskInstancesColumn.tsx
index ed47572760c..227e8ed0188 100644
--- 
a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/TaskInstancesColumn.tsx
+++ 
b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/TaskInstancesColumn.tsx
@@ -152,6 +152,7 @@ export const TaskInstancesColumn = ({
             )}
             <GridTI
               dagId={dagId}
+              hasNote={taskInstance.has_note ?? false}
               instance={taskInstance}
               isGroup={node.isGroup}
               isMapped={node.is_mapped}
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_grid.py 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_grid.py
index d55b2df3625..7466812ca6c 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_grid.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_grid.py
@@ -30,7 +30,7 @@ from airflow._shared.timezones import timezone
 from airflow.models.dag import DagModel
 from airflow.models.dag_version import DagVersion
 from airflow.models.dagbag import DBDagBag
-from airflow.models.taskinstance import TaskInstance
+from airflow.models.taskinstance import TaskInstance, TaskInstanceNote
 from airflow.providers.standard.operators.empty import EmptyOperator
 from airflow.providers.standard.operators.python import PythonOperator
 from airflow.sdk import task_group
@@ -738,6 +738,7 @@ class TestGetGridDataEndpoint:
                     "task_display_name": "t1",
                     "child_states": None,
                     "dag_version_number": 1,
+                    "has_note": False,
                     "max_end_date": "2025-03-02T00:00:00Z",
                     "min_start_date": "2025-03-01T23:59:58Z",
                 },
@@ -747,6 +748,7 @@ class TestGetGridDataEndpoint:
                     "task_display_name": "t2",
                     "child_states": None,
                     "dag_version_number": 1,
+                    "has_note": False,
                     "max_end_date": "2025-03-02T00:00:02Z",
                     "min_start_date": "2025-03-02T00:00:00Z",
                 },
@@ -756,12 +758,14 @@ class TestGetGridDataEndpoint:
                     "task_display_name": "t7",
                     "child_states": None,
                     "dag_version_number": 1,
+                    "has_note": False,
                     "max_end_date": "2025-03-02T00:00:04Z",
                     "min_start_date": "2025-03-02T00:00:02Z",
                 },
                 {
                     "child_states": {"success": 4},
                     "dag_version_number": 1,
+                    "has_note": False,
                     "max_end_date": "2025-03-02T00:00:12Z",
                     "min_start_date": "2025-03-02T00:00:04Z",
                     "state": "success",
@@ -774,12 +778,14 @@ class TestGetGridDataEndpoint:
                     "task_display_name": "task_group-1.t6",
                     "child_states": None,
                     "dag_version_number": 1,
+                    "has_note": False,
                     "max_end_date": "2025-03-02T00:00:06Z",
                     "min_start_date": "2025-03-02T00:00:04Z",
                 },
                 {
                     "child_states": {"success": 3},
                     "dag_version_number": 1,
+                    "has_note": False,
                     "max_end_date": "2025-03-02T00:00:12Z",
                     "min_start_date": "2025-03-02T00:00:06Z",
                     "state": "success",
@@ -792,6 +798,7 @@ class TestGetGridDataEndpoint:
                     "task_display_name": "task_group-1.task_group-2.t3",
                     "child_states": None,
                     "dag_version_number": 1,
+                    "has_note": False,
                     "max_end_date": "2025-03-02T00:00:08Z",
                     "min_start_date": "2025-03-02T00:00:06Z",
                 },
@@ -801,6 +808,7 @@ class TestGetGridDataEndpoint:
                     "task_display_name": "task_group-1.task_group-2.t4",
                     "child_states": None,
                     "dag_version_number": 1,
+                    "has_note": False,
                     "max_end_date": "2025-03-02T00:00:10Z",
                     "min_start_date": "2025-03-02T00:00:08Z",
                 },
@@ -810,6 +818,7 @@ class TestGetGridDataEndpoint:
                     "task_display_name": "task_group-1.task_group-2.t5",
                     "child_states": None,
                     "dag_version_number": 1,
+                    "has_note": False,
                     "max_end_date": "2025-03-02T00:00:12Z",
                     "min_start_date": "2025-03-02T00:00:10Z",
                 },
@@ -842,6 +851,7 @@ class TestGetGridDataEndpoint:
             {
                 "child_states": {"none": 1},
                 "dag_version_number": 1,
+                "has_note": False,
                 "task_id": "mapped_task_2",
                 "task_display_name": "mapped_task_2",
                 "max_end_date": None,
@@ -851,6 +861,7 @@ class TestGetGridDataEndpoint:
             {
                 "child_states": {"success": 1, "running": 1, "none": 1},
                 "dag_version_number": 1,
+                "has_note": False,
                 "max_end_date": "2024-12-30T01:02:03Z",
                 "min_start_date": "2024-12-30T01:00:00Z",
                 "state": "running",
@@ -863,6 +874,7 @@ class TestGetGridDataEndpoint:
                 "task_display_name": "mapped_task_group.subtask",
                 "child_states": None,
                 "dag_version_number": 1,
+                "has_note": False,
                 "max_end_date": "2024-12-30T01:02:03Z",
                 "min_start_date": "2024-12-30T01:00:00Z",
             },
@@ -872,12 +884,14 @@ class TestGetGridDataEndpoint:
                 "task_display_name": "A Beautiful Task Name \U0001f680",
                 "child_states": None,
                 "dag_version_number": 1,
+                "has_note": False,
                 "max_end_date": None,
                 "min_start_date": None,
             },
             {
                 "child_states": {"none": 6},
                 "dag_version_number": 1,
+                "has_note": False,
                 "task_id": "task_group",
                 "task_display_name": "task_group",
                 "max_end_date": None,
@@ -887,6 +901,7 @@ class TestGetGridDataEndpoint:
             {
                 "child_states": {"none": 2},
                 "dag_version_number": 1,
+                "has_note": False,
                 "task_id": "task_group.inner_task_group",
                 "task_display_name": "task_group.inner_task_group",
                 "max_end_date": None,
@@ -896,6 +911,7 @@ class TestGetGridDataEndpoint:
             {
                 "child_states": {"none": 2},
                 "dag_version_number": 1,
+                "has_note": False,
                 "task_id": 
"task_group.inner_task_group.inner_task_group_sub_task",
                 "task_display_name": "Inner Task Group Sub Task Label",
                 "max_end_date": None,
@@ -905,6 +921,7 @@ class TestGetGridDataEndpoint:
             {
                 "child_states": {"none": 4},
                 "dag_version_number": 1,
+                "has_note": False,
                 "task_id": "task_group.mapped_task",
                 "task_display_name": "task_group.mapped_task",
                 "max_end_date": None,
@@ -1143,6 +1160,22 @@ class TestGetGridDataEndpoint:
         task_ids = sorted([node["id"] for node in nodes])
         assert task_ids == expected_task_ids, description
 
+    def test_grid_ti_summaries_has_note(self, session, test_client):
+        """has_note is true when a TaskInstanceNote exists for a TI, false 
otherwise."""
+        ti = session.scalar(
+            select(TaskInstance).where(TaskInstance.run_id == "run_1", 
TaskInstance.task_id == TASK_ID)
+        )
+        ti.task_instance_note = TaskInstanceNote(content="test note")
+        session.commit()
+
+        response = 
test_client.get(f"/grid/ti_summaries/{DAG_ID}?run_ids=run_1")
+        assert response.status_code == 200
+        [data] = self._parse_ndjson(response)
+        by_task_id = {ti["task_id"]: ti for ti in data["task_instances"]}
+
+        assert by_task_id[TASK_ID]["has_note"] is True
+        assert all(not ti["has_note"] for task_id, ti in by_task_id.items() if 
task_id != TASK_ID)
+
     @staticmethod
     def _parse_ndjson(response) -> list[dict]:
         """Parse NDJSON streaming response into a list of dicts."""

Reply via email to