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.