Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3130,6 +3130,10 @@ components:
- type: integer
- type: 'null'
title: Dag Version Number
has_note:
type: boolean
title: Has Note
default: false
type: object
required:
- task_id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
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:
Expand Down Expand Up @@ -419,6 +419,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
Expand Down Expand Up @@ -513,6 +514,13 @@ def _generate() -> Generator[str, None, None]:
# 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(
Expand All @@ -523,6 +531,7 @@ def _generate() -> Generator[str, None, None]:
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -51,6 +52,7 @@ def add_ti(
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
Expand All @@ -62,6 +64,7 @@ def add_ti(
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."""
Expand All @@ -78,6 +81,7 @@ def merge(self, other: GridNodeAgg) -> None:
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."""
Expand Down Expand Up @@ -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,
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9870,6 +9870,11 @@ export const $LightGridTaskInstanceSummary = {
}
],
title: 'Dag Version Number'
},
has_note: {
type: 'boolean',
title: 'Has Note',
default: false
}
},
type: 'object',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2497,6 +2497,7 @@ export type LightGridTaskInstanceSummary = {
min_start_date: string | null;
max_end_date: string | null;
dag_version_number?: number | null;
has_note?: boolean;
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,12 @@ import { StateIcon } from "src/components/StateIcon";
import TaskInstanceTooltip from "src/components/TaskInstanceTooltip";
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;
Expand All @@ -35,7 +39,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 { groupId: selectedGroupId, taskId: selectedTaskId } = useParams();
const location = useLocation();

Expand Down Expand Up @@ -101,6 +114,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"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ export const TaskInstancesColumn = ({
)}
<GridTI
dagId={dagId}
hasNote={taskInstance.has_note ?? false}
instance={taskInstance}
isGroup={node.isGroup}
isMapped={node.is_mapped}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
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
Expand Down Expand Up @@ -771,6 +771,7 @@ def test_grid_ti_summaries_group(self, session, test_client):
"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",
},
Expand All @@ -780,6 +781,7 @@ def test_grid_ti_summaries_group(self, session, test_client):
"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",
},
Expand All @@ -789,12 +791,14 @@ def test_grid_ti_summaries_group(self, session, test_client):
"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",
Expand All @@ -807,12 +811,14 @@ def test_grid_ti_summaries_group(self, session, test_client):
"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",
Expand All @@ -825,6 +831,7 @@ def test_grid_ti_summaries_group(self, session, test_client):
"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",
},
Expand All @@ -834,6 +841,7 @@ def test_grid_ti_summaries_group(self, session, test_client):
"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",
},
Expand All @@ -843,6 +851,7 @@ def test_grid_ti_summaries_group(self, session, test_client):
"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",
},
Expand Down Expand Up @@ -875,6 +884,7 @@ def sort_dict(in_dict):
{
"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,
Expand All @@ -884,6 +894,7 @@ def sort_dict(in_dict):
{
"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",
Expand All @@ -896,6 +907,7 @@ def sort_dict(in_dict):
"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",
},
Expand All @@ -905,12 +917,14 @@ def sort_dict(in_dict):
"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,
Expand All @@ -920,6 +934,7 @@ def sort_dict(in_dict):
{
"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,
Expand All @@ -929,6 +944,7 @@ def sort_dict(in_dict):
{
"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,
Expand All @@ -938,6 +954,7 @@ def sort_dict(in_dict):
{
"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,
Expand Down Expand Up @@ -1176,6 +1193,22 @@ def test_structure_with_depth(self, test_client, dag_id, params, expected_task_i
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."""
Expand Down
Loading