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 87bf0236d92 Fix blank Assets dependency graph from missing Dag nodes 
(#69171)
87bf0236d92 is described below

commit 87bf0236d924c9dd9feb19e21d8a236844905848
Author: davidnzhang <[email protected]>
AuthorDate: Fri Jul 3 01:53:31 2026 +1000

    Fix blank Assets dependency graph from missing Dag nodes (#69171)
---
 .../core_api/services/ui/dependencies.py           | 13 ++++++++++
 .../core_api/routes/ui/test_dependencies.py        | 30 ++++++++++++++++++++++
 2 files changed, 43 insertions(+)

diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/services/ui/dependencies.py 
b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/dependencies.py
index eb4e8e20910..20ffc78008a 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/services/ui/dependencies.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/dependencies.py
@@ -135,6 +135,19 @@ def get_scheduling_dependencies(readable_dag_ids: set[str] 
| None = None) -> dic
                     target = dep.target if ":" in dep.target else 
f"dag:{dep.target}"
                     edge_tuples.add((source, target))
 
+    # Create missing ``dag:`` nodes which may have been skipped by the loop 
above.
+    # A DAG referenced only as a trigger target or a sensor source may have no
+    # scheduling dependencies of its own. Without this loop, these DAGs will 
not be
+    # materialised and will result in dangling edges.
+    for source, target in edge_tuples:
+        for endpoint in (source, target):
+            if endpoint.startswith("dag:") and endpoint not in nodes_dict:
+                nodes_dict[endpoint] = {
+                    "id": endpoint,
+                    "label": endpoint.removeprefix("dag:"),
+                    "type": "dag",
+                }
+
     dag_ids = [node["label"] for node in nodes_dict.values() if node["type"] 
== "dag"]
     if dag_ids:
         dag_id_to_team = DagModel.get_dag_id_to_team_name_mapping(dag_ids)
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dependencies.py 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dependencies.py
index 513290accb9..eca2fa00875 100644
--- 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dependencies.py
+++ 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dependencies.py
@@ -147,6 +147,12 @@ def expected_primary_component_response(asset1_id):
                 "type": "dag",
                 "team": None,
             },
+            {
+                "id": "dag:other_dag",
+                "label": "other_dag",
+                "type": "dag",
+                "team": None,
+            },
         ],
     }
 
@@ -551,3 +557,27 @@ class TestGetDependencies:
         task_nodes = [node for node in result["nodes"] if node["type"] == 
"task"]
         assert len(task_nodes) == 1
         assert task_nodes[0]["team"] == "data-team"
+
+    def 
test_trigger_target_without_scheduling_dependencies_is_materialised(self, 
dag_maker, test_client):
+        """A DAG triggered via TriggerDagRunOperator that has no scheduling 
dependencies must still appear as a node"""
+        with dag_maker(dag_id="parent_dag", serialized=True):
+            TriggerDagRunOperator(task_id="trigger_child_dag", 
trigger_dag_id="child_dag")
+
+        with dag_maker(dag_id="child_dag", serialized=True):
+            EmptyOperator(task_id="some_task")
+
+        dag_maker.sync_dagbag_to_db()
+
+        response = test_client.get("/dependencies")
+        assert response.status_code == 200
+
+        result = response.json()
+        node_ids = {node["id"] for node in result["nodes"]}
+        assert "dag:child_dag" in node_ids
+
+        expected_edge = (
+            "trigger:parent_dag:child_dag:trigger_child_dag",
+            "dag:child_dag",
+        )
+        actual_edges = {(edge["source_id"], edge["target_id"]) for edge in 
result["edges"]}
+        assert expected_edge in actual_edges

Reply via email to