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

bbovenzi 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 66270542726 Add `has_note` key to the Grid Runs API response (#69121)
66270542726 is described below

commit 66270542726dae12b5decc197be3b249bcafdb48
Author: Jayachandra Kasarla <[email protected]>
AuthorDate: Thu Jul 9 22:57:48 2026 +0530

    Add `has_note` key to the Grid Runs API response (#69121)
    
    * Added has_note attribute to get_grid_runs() response
    
    * Added tests for has_note
    
    * Added note gradient to Bar.tsx to render saved note indicator for a dag 
run
    
    * Modified NOTE_GRADIENT to render constant size note indicator
    
    * modified note gradient comment in constants.ts
    
    * Added null check for DagRun note content
    
    * Added saved note content check for TaskInstanceNote subq
    
    * Added has_note to makeRun in useGridPagination.test.ts
---
 .../api_fastapi/core_api/datamodels/ui/common.py        |  1 +
 .../api_fastapi/core_api/openapi/_private_ui.yaml       |  4 ++++
 .../src/airflow/api_fastapi/core_api/routes/ui/grid.py  | 17 ++++++++++++-----
 .../src/airflow/ui/openapi-gen/requests/schemas.gen.ts  |  6 +++++-
 .../src/airflow/ui/openapi-gen/requests/types.gen.ts    |  1 +
 .../src/airflow/ui/src/layouts/Details/Grid/Bar.tsx     |  4 +++-
 .../src/airflow/ui/src/layouts/Details/Grid/GridTI.tsx  |  3 +--
 .../airflow/ui/src/layouts/Details/Grid/constants.ts    |  4 ++++
 .../src/layouts/Details/Grid/useGridPagination.test.ts  |  1 +
 .../unit/api_fastapi/core_api/routes/ui/test_grid.py    | 16 ++++++++++++++++
 10 files changed, 48 insertions(+), 9 deletions(-)

diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/common.py 
b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/common.py
index e4296730017..702f6997cdf 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/common.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/common.py
@@ -93,6 +93,7 @@ class GridRunsResponse(BaseModel):
     run_type: DagRunType
     dag_versions: list[DagVersionResponse] = []
     has_missed_deadline: bool
+    has_note: bool
 
     @computed_field
     def duration(self) -> float:
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 3defcad70f9..e01a6f6e3c5 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
@@ -3188,6 +3188,9 @@ components:
         has_missed_deadline:
           type: boolean
           title: Has Missed Deadline
+        has_note:
+          type: boolean
+          title: Has Note
         duration:
           type: number
           title: Duration
@@ -3203,6 +3206,7 @@ components:
       - state
       - run_type
       - has_missed_deadline
+      - has_note
       - duration
       title: GridRunsResponse
       description: Base Node serializer for responses.
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 ea9de387c39..a16f9336fd5 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
@@ -65,7 +65,7 @@ from airflow.api_fastapi.core_api.services.ui.task_group 
import (
     task_group_to_dict_grid,
 )
 from airflow.models.dag_version import DagVersion
-from airflow.models.dagrun import DagRun
+from airflow.models.dagrun import DagRun, DagRunNote
 from airflow.models.deadline import Deadline
 from airflow.models.serialized_dag import SerializedDagModel
 from airflow.models.taskinstance import TaskInstance, TaskInstanceNote
@@ -291,8 +291,14 @@ def get_grid_runs(
         .correlate(DagRun)
         .label("has_missed_deadline")
     )
+    has_note_subq = (
+        exists()
+        .where(DagRunNote.dag_run_id == DagRun.id, 
DagRunNote.content.isnot(None))
+        .correlate(DagRun)
+        .label("has_note")
+    )
     base_query = (
-        select(DagRun, has_missed_deadline)
+        select(DagRun, has_missed_deadline, has_note_subq)
         .where(DagRun.dag_id == dag_id)
         .options(
             load_only(
@@ -329,10 +335,10 @@ def get_grid_runs(
         return_total_entries=False,
     )
     results = session.execute(dag_runs_select_filter).unique().all()
-    dag_runs = [run for run, _ in results]
+    dag_runs = [run for run, _, _ in results]
     attach_dag_versions_to_runs(dag_runs, session=session)
     grid_runs = []
-    for run, has_missed in results:
+    for run, has_missed, has_note in results:
         grid_runs.append(
             GridRunsResponse.model_validate(
                 {
@@ -346,6 +352,7 @@ def get_grid_runs(
                     "run_type": run.run_type,
                     "dag_versions": run.dag_versions,
                     "has_missed_deadline": has_missed,
+                    "has_note": has_note,
                 }
             )
         )
@@ -470,7 +477,7 @@ def get_grid_ti_summaries_stream(
 
         has_note_subq = (
             exists()
-            .where(TaskInstanceNote.ti_id == TaskInstance.id)
+            .where(TaskInstanceNote.ti_id == TaskInstance.id, 
TaskInstanceNote.content.isnot(None))
             .correlate(TaskInstance)
             .label("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 956f4f9c9ae..df738113acc 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
@@ -9910,6 +9910,10 @@ export const $GridRunsResponse = {
             type: 'boolean',
             title: 'Has Missed Deadline'
         },
+        has_note: {
+            type: 'boolean',
+            title: 'Has Note'
+        },
         duration: {
             type: 'number',
             title: 'Duration',
@@ -9917,7 +9921,7 @@ export const $GridRunsResponse = {
         }
     },
     type: 'object',
-    required: ['dag_id', 'run_id', 'queued_at', 'start_date', 'end_date', 
'run_after', 'state', 'run_type', 'has_missed_deadline', 'duration'],
+    required: ['dag_id', 'run_id', 'queued_at', 'start_date', 'end_date', 
'run_after', 'state', 'run_type', 'has_missed_deadline', 'has_note', 
'duration'],
     title: 'GridRunsResponse',
     description: 'Base Node serializer for responses.'
 } as const;
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 85c405ae215..111ba9204ef 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
@@ -2529,6 +2529,7 @@ export type GridRunsResponse = {
     run_type: DagRunType;
     dag_versions?: Array<DagVersionResponse>;
     has_missed_deadline: boolean;
+    has_note: boolean;
     readonly duration: number;
 };
 
diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Bar.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Bar.tsx
index 0cfce328ac4..acb26f4a9b1 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Bar.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Bar.tsx
@@ -25,7 +25,7 @@ import { useHover } from "src/context/hover";
 
 import { GridButton } from "./GridButton";
 import { BundleVersionIndicator, DagVersionIndicator } from 
"./VersionIndicator";
-import { BAR_HEIGHT } from "./constants";
+import { BAR_HEIGHT, NOTE_GRADIENT } from "./constants";
 import {
   getBundleVersion,
   getMaxVersionNumber,
@@ -83,6 +83,7 @@ export const Bar = ({ max, onClick, run, 
showVersionIndicatorMode }: Props) => {
         <GridButton
           alignItems="center"
           color="fg"
+          colorPalette={run.state ?? "none"}
           dagId={dagId}
           duration={run.duration}
           flexDir="column"
@@ -93,6 +94,7 @@ export const Bar = ({ max, onClick, run, 
showVersionIndicatorMode }: Props) => {
           runId={run.run_id}
           searchParams={search}
           state={run.state}
+          style={run.has_note ? { background: NOTE_GRADIENT } : undefined}
           zIndex={1}
         >
           {run.run_type !== "scheduled" && <RunTypeIcon color="white" 
runType={run.run_type} size="10px" />}
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 4ced7ca10c1..f7f1abaa74c 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,7 @@ 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%)";
+import { NOTE_GRADIENT } from "./constants";
 
 type Props = {
   readonly dagId: string;
diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/constants.ts 
b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/constants.ts
index 616dd2223d9..fa2f987fffb 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/constants.ts
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/constants.ts
@@ -41,3 +41,7 @@ export const BUNDLE_VERSION_INDICATOR_LEFT = -2; // Position 
from left for bundl
 export const BUNDLE_VERSION_ICON_SIZE = 15; // Size of the git commit icon
 export const DAG_VERSION_INDICATOR_HEIGHT = 104; // Height of the vertical 
line indicator
 export const VERSION_INDICATOR_Z_INDEX = 1; // Z-index for version indicators
+
+// Render a gradient to indicate a saved note on a Dag run or task instance
+export const NOTE_GRADIENT =
+  "linear-gradient(45deg, var(--chakra-colors-color-palette-solid) calc(100% - 
8px), var(--chakra-colors-color-palette-emphasized) calc(100% - 8px))";
diff --git 
a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/useGridPagination.test.ts
 
b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/useGridPagination.test.ts
index b8fae1ed1f2..33a40c8e366 100644
--- 
a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/useGridPagination.test.ts
+++ 
b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/useGridPagination.test.ts
@@ -28,6 +28,7 @@ const makeRun = (runId: string): GridRunsResponse => ({
   duration: 0,
   end_date: null,
   has_missed_deadline: false,
+  has_note: false,
   queued_at: null,
   run_after: "2024-01-01T00:00:00Z",
   run_id: runId,
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 7466812ca6c..17b485b9ebf 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,6 +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.dagrun import DagRun, DagRunNote
 from airflow.models.taskinstance import TaskInstance, TaskInstanceNote
 from airflow.providers.standard.operators.empty import EmptyOperator
 from airflow.providers.standard.operators.python import PythonOperator
@@ -76,6 +77,7 @@ GRID_RUN_1 = {
     "duration": 283996800.0,
     "end_date": "2024-12-31T00:00:00Z",
     "has_missed_deadline": False,
+    "has_note": False,
     "run_after": "2024-11-30T00:00:00Z",
     "run_id": "run_1",
     "run_type": "scheduled",
@@ -97,6 +99,7 @@ GRID_RUN_2 = {
     "duration": 283996800.0,
     "end_date": "2024-12-31T00:00:00Z",
     "has_missed_deadline": False,
+    "has_note": False,
     "run_after": "2024-11-30T00:00:00Z",
     "run_id": "run_2",
     "run_type": "manual",
@@ -643,6 +646,19 @@ class TestGetGridDataEndpoint:
         assert response.status_code == 200
         assert _strip_dag_version_ids(response.json()) == [GRID_RUN_1, 
GRID_RUN_2]
 
+    def test_get_grid_runs_has_note(self, session, test_client):
+        """has_note is True when a DagRunNote exists for a dag run, False 
otherwise."""
+        run_1 = session.scalar(select(DagRun).where(DagRun.dag_id == DAG_ID, 
DagRun.run_id == "run_1"))
+        run_1.dag_run_note = DagRunNote(content="a note on run_1")
+        session.commit()
+
+        response = test_client.get(f"/grid/runs/{DAG_ID}")
+        assert response.status_code == 200
+        by_run_id = {run["run_id"]: run for run in response.json()}
+
+        assert by_run_id["run_1"]["has_note"] is True
+        assert by_run_id["run_2"]["has_note"] is False
+
     def test_get_grid_runs_multiple_dag_versions(self, session, test_client):
         # run_5_2 is created after version 2 exists, so its task instances run 
on version 2.
         # Reassign one of them to version 1 so the run spans two versions.

Reply via email to