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 69cd23722d7 AIP-84 Migrate private graph_data endpoint (#44394)
69cd23722d7 is described below

commit 69cd23722d79bf19ee36f9ff57c6a822e87ba1f1
Author: Pierre Jeambrun <[email protected]>
AuthorDate: Thu Nov 28 23:11:13 2024 +0800

    AIP-84 Migrate private graph_data endpoint (#44394)
    
    * Working basic version
    
    * Add tests
---
 .../api_fastapi/core_api/datamodels/ui/graph.py    |  60 +++++
 .../api_fastapi/core_api/openapi/v1-generated.yaml | 251 ++++++++++++++++++---
 airflow/api_fastapi/core_api/routes/ui/__init__.py |   6 +-
 .../api_fastapi/core_api/routes/ui/dashboard.py    |   4 +-
 airflow/api_fastapi/core_api/routes/ui/graph.py    |  66 ++++++
 airflow/ui/openapi-gen/queries/common.ts           | 128 ++++++-----
 airflow/ui/openapi-gen/queries/prefetch.ts         | 149 +++++++-----
 airflow/ui/openapi-gen/queries/queries.ts          | 171 ++++++++------
 airflow/ui/openapi-gen/queries/suspense.ts         | 171 ++++++++------
 airflow/ui/openapi-gen/requests/schemas.gen.ts     | 195 ++++++++++++++++
 airflow/ui/openapi-gen/requests/services.gen.ts    | 172 ++++++++------
 airflow/ui/openapi-gen/requests/types.gen.ts       | 171 ++++++++++----
 tests/api_fastapi/core_api/routes/ui/test_graph.py | 198 ++++++++++++++++
 13 files changed, 1352 insertions(+), 390 deletions(-)

diff --git a/airflow/api_fastapi/core_api/datamodels/ui/graph.py 
b/airflow/api_fastapi/core_api/datamodels/ui/graph.py
new file mode 100644
index 00000000000..b4d7587b35e
--- /dev/null
+++ b/airflow/api_fastapi/core_api/datamodels/ui/graph.py
@@ -0,0 +1,60 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from typing import Literal
+
+from airflow.api_fastapi.core_api.base import BaseModel
+
+
+class EdgeResponse(BaseModel):
+    """Edge serializer for responses."""
+
+    is_setup_teardown: bool | None = None
+    label: str | None = None
+    source_id: str
+    target_id: str
+
+
+class NodeValueResponse(BaseModel):
+    """Graph Node Value responses."""
+
+    isMapped: bool | None = None
+    label: str | None = None
+    labelStyle: str | None = None
+    style: str | None = None
+    tooltip: str | None = None
+    rx: int
+    ry: int
+    clusterLabelPos: str | None = None
+    setupTeardownType: Literal["setup", "teardown"] | None = None
+
+
+class NodeResponse(BaseModel):
+    """Node serializer for responses."""
+
+    children: list[NodeResponse] | None = None
+    id: str | None
+    value: NodeValueResponse
+
+
+class GraphDataResponse(BaseModel):
+    """Graph Data serializer for responses."""
+
+    edges: list[EdgeResponse]
+    nodes: NodeResponse
+    arrange: Literal["BT", "LR", "RL", "TB"]
diff --git a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml 
b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml
index 25ade1d9171..700f65a1b9d 100644
--- a/airflow/api_fastapi/core_api/openapi/v1-generated.yaml
+++ b/airflow/api_fastapi/core_api/openapi/v1-generated.yaml
@@ -34,47 +34,26 @@ paths:
             application/json:
               schema:
                 $ref: '#/components/schemas/HTTPValidationError'
-  /ui/dashboard/historical_metrics_data:
+  /ui/config:
     get:
       tags:
-      - Dashboard
-      summary: Historical Metrics
-      description: Return cluster activity historical metrics.
-      operationId: historical_metrics
-      parameters:
-      - name: start_date
-        in: query
-        required: true
-        schema:
-          type: string
-          title: Start Date
-      - name: end_date
-        in: query
-        required: false
-        schema:
-          anyOf:
-          - type: string
-          - type: 'null'
-          title: End Date
+      - Config
+      summary: Get Configs
+      description: Get configs for UI.
+      operationId: get_configs
       responses:
         '200':
           description: Successful Response
           content:
             application/json:
               schema:
-                $ref: '#/components/schemas/HistoricalMetricDataResponse'
-        '400':
+                $ref: '#/components/schemas/ConfigResponse'
+        '404':
+          description: Not Found
           content:
             application/json:
               schema:
                 $ref: '#/components/schemas/HTTPExceptionResponse'
-          description: Bad Request
-        '422':
-          description: Validation Error
-          content:
-            application/json:
-              schema:
-                $ref: '#/components/schemas/HTTPValidationError'
   /ui/dags/recent_dag_runs:
     get:
       tags:
@@ -174,26 +153,102 @@ paths:
             application/json:
               schema:
                 $ref: '#/components/schemas/HTTPValidationError'
-  /ui/config:
+  /ui/dashboard/historical_metrics_data:
     get:
       tags:
-      - Config
-      summary: Get Configs
-      description: Get configs for UI.
-      operationId: get_configs
+      - Dashboard
+      summary: Historical Metrics
+      description: Return cluster activity historical metrics.
+      operationId: historical_metrics
+      parameters:
+      - name: start_date
+        in: query
+        required: true
+        schema:
+          type: string
+          title: Start Date
+      - name: end_date
+        in: query
+        required: false
+        schema:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: End Date
       responses:
         '200':
           description: Successful Response
           content:
             application/json:
               schema:
-                $ref: '#/components/schemas/ConfigResponse'
-        '404':
-          description: Not Found
+                $ref: '#/components/schemas/HistoricalMetricDataResponse'
+        '400':
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/HTTPExceptionResponse'
+          description: Bad Request
+        '422':
+          description: Validation Error
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/HTTPValidationError'
+  /ui/graph/graph_data:
+    get:
+      tags:
+      - Graph
+      summary: Graph Data
+      description: Get Graph Data.
+      operationId: graph_data
+      parameters:
+      - name: dag_id
+        in: query
+        required: true
+        schema:
+          type: string
+          title: Dag Id
+      - name: root
+        in: query
+        required: false
+        schema:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Root
+      - name: include_upstream
+        in: query
+        required: false
+        schema:
+          type: boolean
+          default: false
+          title: Include Upstream
+      - name: include_downstream
+        in: query
+        required: false
+        schema:
+          type: boolean
+          default: false
+          title: Include Downstream
+      responses:
+        '200':
+          description: Successful Response
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/GraphDataResponse'
+        '400':
           content:
             application/json:
               schema:
                 $ref: '#/components/schemas/HTTPExceptionResponse'
+          description: Bad Request
+        '422':
+          description: Validation Error
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/HTTPValidationError'
   /public/assets:
     get:
       tags:
@@ -7296,6 +7351,30 @@ components:
         This is the set of allowable values for the ``warning_type`` field
 
         in the DagWarning model.'
+    EdgeResponse:
+      properties:
+        is_setup_teardown:
+          anyOf:
+          - type: boolean
+          - type: 'null'
+          title: Is Setup Teardown
+        label:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Label
+        source_id:
+          type: string
+          title: Source Id
+        target_id:
+          type: string
+          title: Target Id
+      type: object
+      required:
+      - source_id
+      - target_id
+      title: EdgeResponse
+      description: Edge serializer for responses.
     EventLogCollectionResponse:
       properties:
         event_logs:
@@ -7407,6 +7486,30 @@ components:
       - name
       title: FastAPIAppResponse
       description: Serializer for Plugin FastAPI App responses.
+    GraphDataResponse:
+      properties:
+        edges:
+          items:
+            $ref: '#/components/schemas/EdgeResponse'
+          type: array
+          title: Edges
+        nodes:
+          $ref: '#/components/schemas/NodeResponse'
+        arrange:
+          type: string
+          enum:
+          - BT
+          - LR
+          - RL
+          - TB
+          title: Arrange
+      type: object
+      required:
+      - edges
+      - nodes
+      - arrange
+      title: GraphDataResponse
+      description: Graph Data serializer for responses.
     HTTPExceptionResponse:
       properties:
         detail:
@@ -7584,6 +7687,80 @@ components:
       - unixname
       title: JobResponse
       description: Job serializer for responses.
+    NodeResponse:
+      properties:
+        children:
+          anyOf:
+          - items:
+              $ref: '#/components/schemas/NodeResponse'
+            type: array
+          - type: 'null'
+          title: Children
+        id:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Id
+        value:
+          $ref: '#/components/schemas/NodeValueResponse'
+      type: object
+      required:
+      - id
+      - value
+      title: NodeResponse
+      description: Node serializer for responses.
+    NodeValueResponse:
+      properties:
+        isMapped:
+          anyOf:
+          - type: boolean
+          - type: 'null'
+          title: Ismapped
+        label:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Label
+        labelStyle:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Labelstyle
+        style:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Style
+        tooltip:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Tooltip
+        rx:
+          type: integer
+          title: Rx
+        ry:
+          type: integer
+          title: Ry
+        clusterLabelPos:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Clusterlabelpos
+        setupTeardownType:
+          anyOf:
+          - type: string
+            enum:
+            - setup
+            - teardown
+          - type: 'null'
+          title: Setupteardowntype
+      type: object
+      required:
+      - rx
+      - ry
+      title: NodeValueResponse
+      description: Graph Node Value responses.
     PatchTaskInstanceBody:
       properties:
         dry_run:
diff --git a/airflow/api_fastapi/core_api/routes/ui/__init__.py 
b/airflow/api_fastapi/core_api/routes/ui/__init__.py
index 156c1219996..0fa150b4653 100644
--- a/airflow/api_fastapi/core_api/routes/ui/__init__.py
+++ b/airflow/api_fastapi/core_api/routes/ui/__init__.py
@@ -21,10 +21,12 @@ from airflow.api_fastapi.core_api.routes.ui.assets import 
assets_router
 from airflow.api_fastapi.core_api.routes.ui.config import config_router
 from airflow.api_fastapi.core_api.routes.ui.dags import dags_router
 from airflow.api_fastapi.core_api.routes.ui.dashboard import dashboard_router
+from airflow.api_fastapi.core_api.routes.ui.graph import graph_data_router
 
 ui_router = AirflowRouter(prefix="/ui")
 
 ui_router.include_router(assets_router)
-ui_router.include_router(dashboard_router)
-ui_router.include_router(dags_router)
 ui_router.include_router(config_router)
+ui_router.include_router(dags_router)
+ui_router.include_router(dashboard_router)
+ui_router.include_router(graph_data_router)
diff --git a/airflow/api_fastapi/core_api/routes/ui/dashboard.py 
b/airflow/api_fastapi/core_api/routes/ui/dashboard.py
index 24682fa0c17..32d86a5caea 100644
--- a/airflow/api_fastapi/core_api/routes/ui/dashboard.py
+++ b/airflow/api_fastapi/core_api/routes/ui/dashboard.py
@@ -35,11 +35,11 @@ from airflow.api_fastapi.common.db.common import get_session
 from airflow.api_fastapi.common.router import AirflowRouter
 from airflow.utils import timezone
 
-dashboard_router = AirflowRouter(tags=["Dashboard"])
+dashboard_router = AirflowRouter(tags=["Dashboard"], prefix="/dashboard")
 
 
 @dashboard_router.get(
-    "/dashboard/historical_metrics_data",
+    "/historical_metrics_data",
     include_in_schema=False,
     responses=create_openapi_http_exception_doc([status.HTTP_400_BAD_REQUEST]),
 )
diff --git a/airflow/api_fastapi/core_api/routes/ui/graph.py 
b/airflow/api_fastapi/core_api/routes/ui/graph.py
new file mode 100644
index 00000000000..338f5904b89
--- /dev/null
+++ b/airflow/api_fastapi/core_api/routes/ui/graph.py
@@ -0,0 +1,66 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Annotated
+
+from fastapi import Depends, Request, status
+from sqlalchemy.orm import Session
+
+from airflow.api_fastapi.core_api.datamodels.ui.graph import GraphDataResponse
+from airflow.api_fastapi.core_api.openapi.exceptions import 
create_openapi_http_exception_doc
+from airflow.utils.dag_edges import dag_edges
+from airflow.utils.task_group import task_group_to_dict
+
+if TYPE_CHECKING:
+    from sqlalchemy.orm import Session
+from airflow.api_fastapi.common.db.common import get_session
+from airflow.api_fastapi.common.router import AirflowRouter
+
+graph_data_router = AirflowRouter(tags=["Graph"], prefix="/graph")
+
+
+@graph_data_router.get(
+    "/graph_data",
+    include_in_schema=False,
+    responses=create_openapi_http_exception_doc([status.HTTP_400_BAD_REQUEST]),
+)
+def graph_data(
+    session: Annotated[Session, Depends(get_session)],
+    dag_id: str,
+    request: Request,
+    root: str | None = None,
+    include_upstream: bool = False,
+    include_downstream: bool = False,
+) -> GraphDataResponse:
+    """Get Graph Data."""
+    dag = request.app.state.dag_bag.get_dag(dag_id)
+    if root:
+        dag = dag.partial_subset(
+            task_ids_or_regex=root, include_upstream=include_upstream, 
include_downstream=include_downstream
+        )
+
+    nodes = task_group_to_dict(dag.task_group)
+    edges = dag_edges(dag)
+
+    data = {
+        "arrange": dag.orientation,
+        "nodes": nodes,
+        "edges": edges,
+    }
+
+    return GraphDataResponse(**data)
diff --git a/airflow/ui/openapi-gen/queries/common.ts 
b/airflow/ui/openapi-gen/queries/common.ts
index cb7f5c7a537..664b7a7202c 100644
--- a/airflow/ui/openapi-gen/queries/common.ts
+++ b/airflow/ui/openapi-gen/queries/common.ts
@@ -15,6 +15,7 @@ import {
   DashboardService,
   EventLogService,
   ExtraLinksService,
+  GraphService,
   ImportErrorService,
   JobService,
   MonitorService,
@@ -200,27 +201,58 @@ export const UseAssetServiceGetDagAssetQueuedEventKeyFn = 
(
   useAssetServiceGetDagAssetQueuedEventKey,
   ...(queryKey ?? [{ before, dagId, uri }]),
 ];
-export type DashboardServiceHistoricalMetricsDefaultResponse = Awaited<
-  ReturnType<typeof DashboardService.historicalMetrics>
+export type ConfigServiceGetConfigsDefaultResponse = Awaited<
+  ReturnType<typeof ConfigService.getConfigs>
 >;
-export type DashboardServiceHistoricalMetricsQueryResult<
-  TData = DashboardServiceHistoricalMetricsDefaultResponse,
+export type ConfigServiceGetConfigsQueryResult<
+  TData = ConfigServiceGetConfigsDefaultResponse,
   TError = unknown,
 > = UseQueryResult<TData, TError>;
-export const useDashboardServiceHistoricalMetricsKey =
-  "DashboardServiceHistoricalMetrics";
-export const UseDashboardServiceHistoricalMetricsKeyFn = (
+export const useConfigServiceGetConfigsKey = "ConfigServiceGetConfigs";
+export const UseConfigServiceGetConfigsKeyFn = (queryKey?: Array<unknown>) => [
+  useConfigServiceGetConfigsKey,
+  ...(queryKey ?? []),
+];
+export type ConfigServiceGetConfigDefaultResponse = Awaited<
+  ReturnType<typeof ConfigService.getConfig>
+>;
+export type ConfigServiceGetConfigQueryResult<
+  TData = ConfigServiceGetConfigDefaultResponse,
+  TError = unknown,
+> = UseQueryResult<TData, TError>;
+export const useConfigServiceGetConfigKey = "ConfigServiceGetConfig";
+export const UseConfigServiceGetConfigKeyFn = (
   {
-    endDate,
-    startDate,
+    accept,
+    section,
   }: {
-    endDate?: string;
-    startDate: string;
+    accept?: "application/json" | "text/plain" | "*/*";
+    section?: string;
+  } = {},
+  queryKey?: Array<unknown>,
+) => [useConfigServiceGetConfigKey, ...(queryKey ?? [{ accept, section }])];
+export type ConfigServiceGetConfigValueDefaultResponse = Awaited<
+  ReturnType<typeof ConfigService.getConfigValue>
+>;
+export type ConfigServiceGetConfigValueQueryResult<
+  TData = ConfigServiceGetConfigValueDefaultResponse,
+  TError = unknown,
+> = UseQueryResult<TData, TError>;
+export const useConfigServiceGetConfigValueKey = "ConfigServiceGetConfigValue";
+export const UseConfigServiceGetConfigValueKeyFn = (
+  {
+    accept,
+    option,
+    section,
+  }: {
+    accept?: "application/json" | "text/plain" | "*/*";
+    option: string;
+    section: string;
   },
   queryKey?: Array<unknown>,
 ) => [
-  useDashboardServiceHistoricalMetricsKey,
-  ...(queryKey ?? [{ endDate, startDate }]),
+  useConfigServiceGetConfigValueKey,
+  ...(queryKey ?? [{ accept, option, section }]),
 ];
 export type DagsServiceRecentDagRunsDefaultResponse = Awaited<
   ReturnType<typeof DagsService.recentDagRuns>
@@ -272,58 +304,52 @@ export const UseDagsServiceRecentDagRunsKeyFn = (
     },
   ]),
 ];
-export type ConfigServiceGetConfigsDefaultResponse = Awaited<
-  ReturnType<typeof ConfigService.getConfigs>
->;
-export type ConfigServiceGetConfigsQueryResult<
-  TData = ConfigServiceGetConfigsDefaultResponse,
-  TError = unknown,
-> = UseQueryResult<TData, TError>;
-export const useConfigServiceGetConfigsKey = "ConfigServiceGetConfigs";
-export const UseConfigServiceGetConfigsKeyFn = (queryKey?: Array<unknown>) => [
-  useConfigServiceGetConfigsKey,
-  ...(queryKey ?? []),
-];
-export type ConfigServiceGetConfigDefaultResponse = Awaited<
-  ReturnType<typeof ConfigService.getConfig>
+export type DashboardServiceHistoricalMetricsDefaultResponse = Awaited<
+  ReturnType<typeof DashboardService.historicalMetrics>
 >;
-export type ConfigServiceGetConfigQueryResult<
-  TData = ConfigServiceGetConfigDefaultResponse,
+export type DashboardServiceHistoricalMetricsQueryResult<
+  TData = DashboardServiceHistoricalMetricsDefaultResponse,
   TError = unknown,
 > = UseQueryResult<TData, TError>;
-export const useConfigServiceGetConfigKey = "ConfigServiceGetConfig";
-export const UseConfigServiceGetConfigKeyFn = (
+export const useDashboardServiceHistoricalMetricsKey =
+  "DashboardServiceHistoricalMetrics";
+export const UseDashboardServiceHistoricalMetricsKeyFn = (
   {
-    accept,
-    section,
+    endDate,
+    startDate,
   }: {
-    accept?: "application/json" | "text/plain" | "*/*";
-    section?: string;
-  } = {},
+    endDate?: string;
+    startDate: string;
+  },
   queryKey?: Array<unknown>,
-) => [useConfigServiceGetConfigKey, ...(queryKey ?? [{ accept, section }])];
-export type ConfigServiceGetConfigValueDefaultResponse = Awaited<
-  ReturnType<typeof ConfigService.getConfigValue>
+) => [
+  useDashboardServiceHistoricalMetricsKey,
+  ...(queryKey ?? [{ endDate, startDate }]),
+];
+export type GraphServiceGraphDataDefaultResponse = Awaited<
+  ReturnType<typeof GraphService.graphData>
 >;
-export type ConfigServiceGetConfigValueQueryResult<
-  TData = ConfigServiceGetConfigValueDefaultResponse,
+export type GraphServiceGraphDataQueryResult<
+  TData = GraphServiceGraphDataDefaultResponse,
   TError = unknown,
 > = UseQueryResult<TData, TError>;
-export const useConfigServiceGetConfigValueKey = "ConfigServiceGetConfigValue";
-export const UseConfigServiceGetConfigValueKeyFn = (
+export const useGraphServiceGraphDataKey = "GraphServiceGraphData";
+export const UseGraphServiceGraphDataKeyFn = (
   {
-    accept,
-    option,
-    section,
+    dagId,
+    includeDownstream,
+    includeUpstream,
+    root,
   }: {
-    accept?: "application/json" | "text/plain" | "*/*";
-    option: string;
-    section: string;
+    dagId: string;
+    includeDownstream?: boolean;
+    includeUpstream?: boolean;
+    root?: string;
   },
   queryKey?: Array<unknown>,
 ) => [
-  useConfigServiceGetConfigValueKey,
-  ...(queryKey ?? [{ accept, option, section }]),
+  useGraphServiceGraphDataKey,
+  ...(queryKey ?? [{ dagId, includeDownstream, includeUpstream, root }]),
 ];
 export type BackfillServiceListBackfillsDefaultResponse = Awaited<
   ReturnType<typeof BackfillService.listBackfills>
diff --git a/airflow/ui/openapi-gen/queries/prefetch.ts 
b/airflow/ui/openapi-gen/queries/prefetch.ts
index 8f423b4c083..6dcb19267f8 100644
--- a/airflow/ui/openapi-gen/queries/prefetch.ts
+++ b/airflow/ui/openapi-gen/queries/prefetch.ts
@@ -15,6 +15,7 @@ import {
   DashboardService,
   EventLogService,
   ExtraLinksService,
+  GraphService,
   ImportErrorService,
   JobService,
   MonitorService,
@@ -248,30 +249,66 @@ export const 
prefetchUseAssetServiceGetDagAssetQueuedEvent = (
     queryFn: () => AssetService.getDagAssetQueuedEvent({ before, dagId, uri }),
   });
 /**
- * Historical Metrics
- * Return cluster activity historical metrics.
+ * Get Configs
+ * Get configs for UI.
+ * @returns ConfigResponse Successful Response
+ * @throws ApiError
+ */
+export const prefetchUseConfigServiceGetConfigs = (queryClient: QueryClient) =>
+  queryClient.prefetchQuery({
+    queryKey: Common.UseConfigServiceGetConfigsKeyFn(),
+    queryFn: () => ConfigService.getConfigs(),
+  });
+/**
+ * Get Config
  * @param data The data for the request.
- * @param data.startDate
- * @param data.endDate
- * @returns HistoricalMetricDataResponse Successful Response
+ * @param data.section
+ * @param data.accept
+ * @returns Config Successful Response
  * @throws ApiError
  */
-export const prefetchUseDashboardServiceHistoricalMetrics = (
+export const prefetchUseConfigServiceGetConfig = (
   queryClient: QueryClient,
   {
-    endDate,
-    startDate,
+    accept,
+    section,
   }: {
-    endDate?: string;
-    startDate: string;
+    accept?: "application/json" | "text/plain" | "*/*";
+    section?: string;
+  } = {},
+) =>
+  queryClient.prefetchQuery({
+    queryKey: Common.UseConfigServiceGetConfigKeyFn({ accept, section }),
+    queryFn: () => ConfigService.getConfig({ accept, section }),
+  });
+/**
+ * Get Config Value
+ * @param data The data for the request.
+ * @param data.section
+ * @param data.option
+ * @param data.accept
+ * @returns Config Successful Response
+ * @throws ApiError
+ */
+export const prefetchUseConfigServiceGetConfigValue = (
+  queryClient: QueryClient,
+  {
+    accept,
+    option,
+    section,
+  }: {
+    accept?: "application/json" | "text/plain" | "*/*";
+    option: string;
+    section: string;
   },
 ) =>
   queryClient.prefetchQuery({
-    queryKey: Common.UseDashboardServiceHistoricalMetricsKeyFn({
-      endDate,
-      startDate,
+    queryKey: Common.UseConfigServiceGetConfigValueKeyFn({
+      accept,
+      option,
+      section,
     }),
-    queryFn: () => DashboardService.historicalMetrics({ endDate, startDate }),
+    queryFn: () => ConfigService.getConfigValue({ accept, option, section }),
   });
 /**
  * Recent Dag Runs
@@ -344,66 +381,70 @@ export const prefetchUseDagsServiceRecentDagRuns = (
       }),
   });
 /**
- * Get Configs
- * Get configs for UI.
- * @returns ConfigResponse Successful Response
- * @throws ApiError
- */
-export const prefetchUseConfigServiceGetConfigs = (queryClient: QueryClient) =>
-  queryClient.prefetchQuery({
-    queryKey: Common.UseConfigServiceGetConfigsKeyFn(),
-    queryFn: () => ConfigService.getConfigs(),
-  });
-/**
- * Get Config
+ * Historical Metrics
+ * Return cluster activity historical metrics.
  * @param data The data for the request.
- * @param data.section
- * @param data.accept
- * @returns Config Successful Response
+ * @param data.startDate
+ * @param data.endDate
+ * @returns HistoricalMetricDataResponse Successful Response
  * @throws ApiError
  */
-export const prefetchUseConfigServiceGetConfig = (
+export const prefetchUseDashboardServiceHistoricalMetrics = (
   queryClient: QueryClient,
   {
-    accept,
-    section,
+    endDate,
+    startDate,
   }: {
-    accept?: "application/json" | "text/plain" | "*/*";
-    section?: string;
-  } = {},
+    endDate?: string;
+    startDate: string;
+  },
 ) =>
   queryClient.prefetchQuery({
-    queryKey: Common.UseConfigServiceGetConfigKeyFn({ accept, section }),
-    queryFn: () => ConfigService.getConfig({ accept, section }),
+    queryKey: Common.UseDashboardServiceHistoricalMetricsKeyFn({
+      endDate,
+      startDate,
+    }),
+    queryFn: () => DashboardService.historicalMetrics({ endDate, startDate }),
   });
 /**
- * Get Config Value
+ * Graph Data
+ * Get Graph Data.
  * @param data The data for the request.
- * @param data.section
- * @param data.option
- * @param data.accept
- * @returns Config Successful Response
+ * @param data.dagId
+ * @param data.root
+ * @param data.includeUpstream
+ * @param data.includeDownstream
+ * @returns GraphDataResponse Successful Response
  * @throws ApiError
  */
-export const prefetchUseConfigServiceGetConfigValue = (
+export const prefetchUseGraphServiceGraphData = (
   queryClient: QueryClient,
   {
-    accept,
-    option,
-    section,
+    dagId,
+    includeDownstream,
+    includeUpstream,
+    root,
   }: {
-    accept?: "application/json" | "text/plain" | "*/*";
-    option: string;
-    section: string;
+    dagId: string;
+    includeDownstream?: boolean;
+    includeUpstream?: boolean;
+    root?: string;
   },
 ) =>
   queryClient.prefetchQuery({
-    queryKey: Common.UseConfigServiceGetConfigValueKeyFn({
-      accept,
-      option,
-      section,
+    queryKey: Common.UseGraphServiceGraphDataKeyFn({
+      dagId,
+      includeDownstream,
+      includeUpstream,
+      root,
     }),
-    queryFn: () => ConfigService.getConfigValue({ accept, option, section }),
+    queryFn: () =>
+      GraphService.graphData({
+        dagId,
+        includeDownstream,
+        includeUpstream,
+        root,
+      }),
   });
 /**
  * List Backfills
diff --git a/airflow/ui/openapi-gen/queries/queries.ts 
b/airflow/ui/openapi-gen/queries/queries.ts
index d644beb7294..024af72e8e8 100644
--- a/airflow/ui/openapi-gen/queries/queries.ts
+++ b/airflow/ui/openapi-gen/queries/queries.ts
@@ -20,6 +20,7 @@ import {
   DashboardService,
   EventLogService,
   ExtraLinksService,
+  GraphService,
   ImportErrorService,
   JobService,
   MonitorService,
@@ -323,36 +324,88 @@ export const useAssetServiceGetDagAssetQueuedEvent = <
     ...options,
   });
 /**
- * Historical Metrics
- * Return cluster activity historical metrics.
+ * Get Configs
+ * Get configs for UI.
+ * @returns ConfigResponse Successful Response
+ * @throws ApiError
+ */
+export const useConfigServiceGetConfigs = <
+  TData = Common.ConfigServiceGetConfigsDefaultResponse,
+  TError = unknown,
+  TQueryKey extends Array<unknown> = unknown[],
+>(
+  queryKey?: TQueryKey,
+  options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">,
+) =>
+  useQuery<TData, TError>({
+    queryKey: Common.UseConfigServiceGetConfigsKeyFn(queryKey),
+    queryFn: () => ConfigService.getConfigs() as TData,
+    ...options,
+  });
+/**
+ * Get Config
  * @param data The data for the request.
- * @param data.startDate
- * @param data.endDate
- * @returns HistoricalMetricDataResponse Successful Response
+ * @param data.section
+ * @param data.accept
+ * @returns Config Successful Response
  * @throws ApiError
  */
-export const useDashboardServiceHistoricalMetrics = <
-  TData = Common.DashboardServiceHistoricalMetricsDefaultResponse,
+export const useConfigServiceGetConfig = <
+  TData = Common.ConfigServiceGetConfigDefaultResponse,
   TError = unknown,
   TQueryKey extends Array<unknown> = unknown[],
 >(
   {
-    endDate,
-    startDate,
+    accept,
+    section,
   }: {
-    endDate?: string;
-    startDate: string;
+    accept?: "application/json" | "text/plain" | "*/*";
+    section?: string;
+  } = {},
+  queryKey?: TQueryKey,
+  options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">,
+) =>
+  useQuery<TData, TError>({
+    queryKey: Common.UseConfigServiceGetConfigKeyFn(
+      { accept, section },
+      queryKey,
+    ),
+    queryFn: () => ConfigService.getConfig({ accept, section }) as TData,
+    ...options,
+  });
+/**
+ * Get Config Value
+ * @param data The data for the request.
+ * @param data.section
+ * @param data.option
+ * @param data.accept
+ * @returns Config Successful Response
+ * @throws ApiError
+ */
+export const useConfigServiceGetConfigValue = <
+  TData = Common.ConfigServiceGetConfigValueDefaultResponse,
+  TError = unknown,
+  TQueryKey extends Array<unknown> = unknown[],
+>(
+  {
+    accept,
+    option,
+    section,
+  }: {
+    accept?: "application/json" | "text/plain" | "*/*";
+    option: string;
+    section: string;
   },
   queryKey?: TQueryKey,
   options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">,
 ) =>
   useQuery<TData, TError>({
-    queryKey: Common.UseDashboardServiceHistoricalMetricsKeyFn(
-      { endDate, startDate },
+    queryKey: Common.UseConfigServiceGetConfigValueKeyFn(
+      { accept, option, section },
       queryKey,
     ),
     queryFn: () =>
-      DashboardService.historicalMetrics({ endDate, startDate }) as TData,
+      ConfigService.getConfigValue({ accept, option, section }) as TData,
     ...options,
   });
 /**
@@ -435,88 +488,80 @@ export const useDagsServiceRecentDagRuns = <
     ...options,
   });
 /**
- * Get Configs
- * Get configs for UI.
- * @returns ConfigResponse Successful Response
- * @throws ApiError
- */
-export const useConfigServiceGetConfigs = <
-  TData = Common.ConfigServiceGetConfigsDefaultResponse,
-  TError = unknown,
-  TQueryKey extends Array<unknown> = unknown[],
->(
-  queryKey?: TQueryKey,
-  options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">,
-) =>
-  useQuery<TData, TError>({
-    queryKey: Common.UseConfigServiceGetConfigsKeyFn(queryKey),
-    queryFn: () => ConfigService.getConfigs() as TData,
-    ...options,
-  });
-/**
- * Get Config
+ * Historical Metrics
+ * Return cluster activity historical metrics.
  * @param data The data for the request.
- * @param data.section
- * @param data.accept
- * @returns Config Successful Response
+ * @param data.startDate
+ * @param data.endDate
+ * @returns HistoricalMetricDataResponse Successful Response
  * @throws ApiError
  */
-export const useConfigServiceGetConfig = <
-  TData = Common.ConfigServiceGetConfigDefaultResponse,
+export const useDashboardServiceHistoricalMetrics = <
+  TData = Common.DashboardServiceHistoricalMetricsDefaultResponse,
   TError = unknown,
   TQueryKey extends Array<unknown> = unknown[],
 >(
   {
-    accept,
-    section,
+    endDate,
+    startDate,
   }: {
-    accept?: "application/json" | "text/plain" | "*/*";
-    section?: string;
-  } = {},
+    endDate?: string;
+    startDate: string;
+  },
   queryKey?: TQueryKey,
   options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">,
 ) =>
   useQuery<TData, TError>({
-    queryKey: Common.UseConfigServiceGetConfigKeyFn(
-      { accept, section },
+    queryKey: Common.UseDashboardServiceHistoricalMetricsKeyFn(
+      { endDate, startDate },
       queryKey,
     ),
-    queryFn: () => ConfigService.getConfig({ accept, section }) as TData,
+    queryFn: () =>
+      DashboardService.historicalMetrics({ endDate, startDate }) as TData,
     ...options,
   });
 /**
- * Get Config Value
+ * Graph Data
+ * Get Graph Data.
  * @param data The data for the request.
- * @param data.section
- * @param data.option
- * @param data.accept
- * @returns Config Successful Response
+ * @param data.dagId
+ * @param data.root
+ * @param data.includeUpstream
+ * @param data.includeDownstream
+ * @returns GraphDataResponse Successful Response
  * @throws ApiError
  */
-export const useConfigServiceGetConfigValue = <
-  TData = Common.ConfigServiceGetConfigValueDefaultResponse,
+export const useGraphServiceGraphData = <
+  TData = Common.GraphServiceGraphDataDefaultResponse,
   TError = unknown,
   TQueryKey extends Array<unknown> = unknown[],
 >(
   {
-    accept,
-    option,
-    section,
+    dagId,
+    includeDownstream,
+    includeUpstream,
+    root,
   }: {
-    accept?: "application/json" | "text/plain" | "*/*";
-    option: string;
-    section: string;
+    dagId: string;
+    includeDownstream?: boolean;
+    includeUpstream?: boolean;
+    root?: string;
   },
   queryKey?: TQueryKey,
   options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">,
 ) =>
   useQuery<TData, TError>({
-    queryKey: Common.UseConfigServiceGetConfigValueKeyFn(
-      { accept, option, section },
+    queryKey: Common.UseGraphServiceGraphDataKeyFn(
+      { dagId, includeDownstream, includeUpstream, root },
       queryKey,
     ),
     queryFn: () =>
-      ConfigService.getConfigValue({ accept, option, section }) as TData,
+      GraphService.graphData({
+        dagId,
+        includeDownstream,
+        includeUpstream,
+        root,
+      }) as TData,
     ...options,
   });
 /**
diff --git a/airflow/ui/openapi-gen/queries/suspense.ts 
b/airflow/ui/openapi-gen/queries/suspense.ts
index 11386ab5d1f..0c109a46ea2 100644
--- a/airflow/ui/openapi-gen/queries/suspense.ts
+++ b/airflow/ui/openapi-gen/queries/suspense.ts
@@ -15,6 +15,7 @@ import {
   DashboardService,
   EventLogService,
   ExtraLinksService,
+  GraphService,
   ImportErrorService,
   JobService,
   MonitorService,
@@ -300,36 +301,88 @@ export const 
useAssetServiceGetDagAssetQueuedEventSuspense = <
     ...options,
   });
 /**
- * Historical Metrics
- * Return cluster activity historical metrics.
+ * Get Configs
+ * Get configs for UI.
+ * @returns ConfigResponse Successful Response
+ * @throws ApiError
+ */
+export const useConfigServiceGetConfigsSuspense = <
+  TData = Common.ConfigServiceGetConfigsDefaultResponse,
+  TError = unknown,
+  TQueryKey extends Array<unknown> = unknown[],
+>(
+  queryKey?: TQueryKey,
+  options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">,
+) =>
+  useSuspenseQuery<TData, TError>({
+    queryKey: Common.UseConfigServiceGetConfigsKeyFn(queryKey),
+    queryFn: () => ConfigService.getConfigs() as TData,
+    ...options,
+  });
+/**
+ * Get Config
  * @param data The data for the request.
- * @param data.startDate
- * @param data.endDate
- * @returns HistoricalMetricDataResponse Successful Response
+ * @param data.section
+ * @param data.accept
+ * @returns Config Successful Response
  * @throws ApiError
  */
-export const useDashboardServiceHistoricalMetricsSuspense = <
-  TData = Common.DashboardServiceHistoricalMetricsDefaultResponse,
+export const useConfigServiceGetConfigSuspense = <
+  TData = Common.ConfigServiceGetConfigDefaultResponse,
   TError = unknown,
   TQueryKey extends Array<unknown> = unknown[],
 >(
   {
-    endDate,
-    startDate,
+    accept,
+    section,
   }: {
-    endDate?: string;
-    startDate: string;
+    accept?: "application/json" | "text/plain" | "*/*";
+    section?: string;
+  } = {},
+  queryKey?: TQueryKey,
+  options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">,
+) =>
+  useSuspenseQuery<TData, TError>({
+    queryKey: Common.UseConfigServiceGetConfigKeyFn(
+      { accept, section },
+      queryKey,
+    ),
+    queryFn: () => ConfigService.getConfig({ accept, section }) as TData,
+    ...options,
+  });
+/**
+ * Get Config Value
+ * @param data The data for the request.
+ * @param data.section
+ * @param data.option
+ * @param data.accept
+ * @returns Config Successful Response
+ * @throws ApiError
+ */
+export const useConfigServiceGetConfigValueSuspense = <
+  TData = Common.ConfigServiceGetConfigValueDefaultResponse,
+  TError = unknown,
+  TQueryKey extends Array<unknown> = unknown[],
+>(
+  {
+    accept,
+    option,
+    section,
+  }: {
+    accept?: "application/json" | "text/plain" | "*/*";
+    option: string;
+    section: string;
   },
   queryKey?: TQueryKey,
   options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">,
 ) =>
   useSuspenseQuery<TData, TError>({
-    queryKey: Common.UseDashboardServiceHistoricalMetricsKeyFn(
-      { endDate, startDate },
+    queryKey: Common.UseConfigServiceGetConfigValueKeyFn(
+      { accept, option, section },
       queryKey,
     ),
     queryFn: () =>
-      DashboardService.historicalMetrics({ endDate, startDate }) as TData,
+      ConfigService.getConfigValue({ accept, option, section }) as TData,
     ...options,
   });
 /**
@@ -412,88 +465,80 @@ export const useDagsServiceRecentDagRunsSuspense = <
     ...options,
   });
 /**
- * Get Configs
- * Get configs for UI.
- * @returns ConfigResponse Successful Response
- * @throws ApiError
- */
-export const useConfigServiceGetConfigsSuspense = <
-  TData = Common.ConfigServiceGetConfigsDefaultResponse,
-  TError = unknown,
-  TQueryKey extends Array<unknown> = unknown[],
->(
-  queryKey?: TQueryKey,
-  options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">,
-) =>
-  useSuspenseQuery<TData, TError>({
-    queryKey: Common.UseConfigServiceGetConfigsKeyFn(queryKey),
-    queryFn: () => ConfigService.getConfigs() as TData,
-    ...options,
-  });
-/**
- * Get Config
+ * Historical Metrics
+ * Return cluster activity historical metrics.
  * @param data The data for the request.
- * @param data.section
- * @param data.accept
- * @returns Config Successful Response
+ * @param data.startDate
+ * @param data.endDate
+ * @returns HistoricalMetricDataResponse Successful Response
  * @throws ApiError
  */
-export const useConfigServiceGetConfigSuspense = <
-  TData = Common.ConfigServiceGetConfigDefaultResponse,
+export const useDashboardServiceHistoricalMetricsSuspense = <
+  TData = Common.DashboardServiceHistoricalMetricsDefaultResponse,
   TError = unknown,
   TQueryKey extends Array<unknown> = unknown[],
 >(
   {
-    accept,
-    section,
+    endDate,
+    startDate,
   }: {
-    accept?: "application/json" | "text/plain" | "*/*";
-    section?: string;
-  } = {},
+    endDate?: string;
+    startDate: string;
+  },
   queryKey?: TQueryKey,
   options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">,
 ) =>
   useSuspenseQuery<TData, TError>({
-    queryKey: Common.UseConfigServiceGetConfigKeyFn(
-      { accept, section },
+    queryKey: Common.UseDashboardServiceHistoricalMetricsKeyFn(
+      { endDate, startDate },
       queryKey,
     ),
-    queryFn: () => ConfigService.getConfig({ accept, section }) as TData,
+    queryFn: () =>
+      DashboardService.historicalMetrics({ endDate, startDate }) as TData,
     ...options,
   });
 /**
- * Get Config Value
+ * Graph Data
+ * Get Graph Data.
  * @param data The data for the request.
- * @param data.section
- * @param data.option
- * @param data.accept
- * @returns Config Successful Response
+ * @param data.dagId
+ * @param data.root
+ * @param data.includeUpstream
+ * @param data.includeDownstream
+ * @returns GraphDataResponse Successful Response
  * @throws ApiError
  */
-export const useConfigServiceGetConfigValueSuspense = <
-  TData = Common.ConfigServiceGetConfigValueDefaultResponse,
+export const useGraphServiceGraphDataSuspense = <
+  TData = Common.GraphServiceGraphDataDefaultResponse,
   TError = unknown,
   TQueryKey extends Array<unknown> = unknown[],
 >(
   {
-    accept,
-    option,
-    section,
+    dagId,
+    includeDownstream,
+    includeUpstream,
+    root,
   }: {
-    accept?: "application/json" | "text/plain" | "*/*";
-    option: string;
-    section: string;
+    dagId: string;
+    includeDownstream?: boolean;
+    includeUpstream?: boolean;
+    root?: string;
   },
   queryKey?: TQueryKey,
   options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">,
 ) =>
   useSuspenseQuery<TData, TError>({
-    queryKey: Common.UseConfigServiceGetConfigValueKeyFn(
-      { accept, option, section },
+    queryKey: Common.UseGraphServiceGraphDataKeyFn(
+      { dagId, includeDownstream, includeUpstream, root },
       queryKey,
     ),
     queryFn: () =>
-      ConfigService.getConfigValue({ accept, option, section }) as TData,
+      GraphService.graphData({
+        dagId,
+        includeDownstream,
+        includeUpstream,
+        root,
+      }) as TData,
     ...options,
   });
 /**
diff --git a/airflow/ui/openapi-gen/requests/schemas.gen.ts 
b/airflow/ui/openapi-gen/requests/schemas.gen.ts
index 84c63a7d7bc..b9289674a18 100644
--- a/airflow/ui/openapi-gen/requests/schemas.gen.ts
+++ b/airflow/ui/openapi-gen/requests/schemas.gen.ts
@@ -2596,6 +2596,45 @@ This is the set of allowable values for the 
\`\`warning_type\`\` field
 in the DagWarning model.`,
 } as const;
 
+export const $EdgeResponse = {
+  properties: {
+    is_setup_teardown: {
+      anyOf: [
+        {
+          type: "boolean",
+        },
+        {
+          type: "null",
+        },
+      ],
+      title: "Is Setup Teardown",
+    },
+    label: {
+      anyOf: [
+        {
+          type: "string",
+        },
+        {
+          type: "null",
+        },
+      ],
+      title: "Label",
+    },
+    source_id: {
+      type: "string",
+      title: "Source Id",
+    },
+    target_id: {
+      type: "string",
+      title: "Target Id",
+    },
+  },
+  type: "object",
+  required: ["source_id", "target_id"],
+  title: "EdgeResponse",
+  description: "Edge serializer for responses.",
+} as const;
+
 export const $EventLogCollectionResponse = {
   properties: {
     event_logs: {
@@ -2777,6 +2816,30 @@ export const $FastAPIAppResponse = {
   description: "Serializer for Plugin FastAPI App responses.",
 } as const;
 
+export const $GraphDataResponse = {
+  properties: {
+    edges: {
+      items: {
+        $ref: "#/components/schemas/EdgeResponse",
+      },
+      type: "array",
+      title: "Edges",
+    },
+    nodes: {
+      $ref: "#/components/schemas/NodeResponse",
+    },
+    arrange: {
+      type: "string",
+      enum: ["BT", "LR", "RL", "TB"],
+      title: "Arrange",
+    },
+  },
+  type: "object",
+  required: ["edges", "nodes", "arrange"],
+  title: "GraphDataResponse",
+  description: "Graph Data serializer for responses.",
+} as const;
+
 export const $HTTPExceptionResponse = {
   properties: {
     detail: {
@@ -3043,6 +3106,138 @@ export const $JobResponse = {
   description: "Job serializer for responses.",
 } as const;
 
+export const $NodeResponse = {
+  properties: {
+    children: {
+      anyOf: [
+        {
+          items: {
+            $ref: "#/components/schemas/NodeResponse",
+          },
+          type: "array",
+        },
+        {
+          type: "null",
+        },
+      ],
+      title: "Children",
+    },
+    id: {
+      anyOf: [
+        {
+          type: "string",
+        },
+        {
+          type: "null",
+        },
+      ],
+      title: "Id",
+    },
+    value: {
+      $ref: "#/components/schemas/NodeValueResponse",
+    },
+  },
+  type: "object",
+  required: ["id", "value"],
+  title: "NodeResponse",
+  description: "Node serializer for responses.",
+} as const;
+
+export const $NodeValueResponse = {
+  properties: {
+    isMapped: {
+      anyOf: [
+        {
+          type: "boolean",
+        },
+        {
+          type: "null",
+        },
+      ],
+      title: "Ismapped",
+    },
+    label: {
+      anyOf: [
+        {
+          type: "string",
+        },
+        {
+          type: "null",
+        },
+      ],
+      title: "Label",
+    },
+    labelStyle: {
+      anyOf: [
+        {
+          type: "string",
+        },
+        {
+          type: "null",
+        },
+      ],
+      title: "Labelstyle",
+    },
+    style: {
+      anyOf: [
+        {
+          type: "string",
+        },
+        {
+          type: "null",
+        },
+      ],
+      title: "Style",
+    },
+    tooltip: {
+      anyOf: [
+        {
+          type: "string",
+        },
+        {
+          type: "null",
+        },
+      ],
+      title: "Tooltip",
+    },
+    rx: {
+      type: "integer",
+      title: "Rx",
+    },
+    ry: {
+      type: "integer",
+      title: "Ry",
+    },
+    clusterLabelPos: {
+      anyOf: [
+        {
+          type: "string",
+        },
+        {
+          type: "null",
+        },
+      ],
+      title: "Clusterlabelpos",
+    },
+    setupTeardownType: {
+      anyOf: [
+        {
+          type: "string",
+          enum: ["setup", "teardown"],
+        },
+        {
+          type: "null",
+        },
+      ],
+      title: "Setupteardowntype",
+    },
+  },
+  type: "object",
+  required: ["rx", "ry"],
+  title: "NodeValueResponse",
+  description: "Graph Node Value responses.",
+} as const;
+
 export const $PatchTaskInstanceBody = {
   properties: {
     dry_run: {
diff --git a/airflow/ui/openapi-gen/requests/services.gen.ts 
b/airflow/ui/openapi-gen/requests/services.gen.ts
index 4c8944447ce..f066d5103be 100644
--- a/airflow/ui/openapi-gen/requests/services.gen.ts
+++ b/airflow/ui/openapi-gen/requests/services.gen.ts
@@ -25,15 +25,17 @@ import type {
   GetDagAssetQueuedEventResponse,
   DeleteDagAssetQueuedEventData,
   DeleteDagAssetQueuedEventResponse,
-  HistoricalMetricsData,
-  HistoricalMetricsResponse,
-  RecentDagRunsData,
-  RecentDagRunsResponse,
   GetConfigsResponse,
   GetConfigData,
   GetConfigResponse,
   GetConfigValueData,
   GetConfigValueResponse,
+  RecentDagRunsData,
+  RecentDagRunsResponse,
+  HistoricalMetricsData,
+  HistoricalMetricsResponse,
+  GraphDataData,
+  GraphDataResponse2,
   ListBackfillsData,
   ListBackfillsResponse,
   CreateBackfillData,
@@ -508,28 +510,80 @@ export class AssetService {
   }
 }
 
-export class DashboardService {
+export class ConfigService {
   /**
-   * Historical Metrics
-   * Return cluster activity historical metrics.
+   * Get Configs
+   * Get configs for UI.
+   * @returns ConfigResponse Successful Response
+   * @throws ApiError
+   */
+  public static getConfigs(): CancelablePromise<GetConfigsResponse> {
+    return __request(OpenAPI, {
+      method: "GET",
+      url: "/ui/config",
+      errors: {
+        404: "Not Found",
+      },
+    });
+  }
+
+  /**
+   * Get Config
    * @param data The data for the request.
-   * @param data.startDate
-   * @param data.endDate
-   * @returns HistoricalMetricDataResponse Successful Response
+   * @param data.section
+   * @param data.accept
+   * @returns Config Successful Response
    * @throws ApiError
    */
-  public static historicalMetrics(
-    data: HistoricalMetricsData,
-  ): CancelablePromise<HistoricalMetricsResponse> {
+  public static getConfig(
+    data: GetConfigData = {},
+  ): CancelablePromise<GetConfigResponse> {
     return __request(OpenAPI, {
       method: "GET",
-      url: "/ui/dashboard/historical_metrics_data",
+      url: "/public/config",
+      headers: {
+        accept: data.accept,
+      },
       query: {
-        start_date: data.startDate,
-        end_date: data.endDate,
+        section: data.section,
       },
       errors: {
-        400: "Bad Request",
+        401: "Unauthorized",
+        403: "Forbidden",
+        404: "Not Found",
+        406: "Not Acceptable",
+        422: "Validation Error",
+      },
+    });
+  }
+
+  /**
+   * Get Config Value
+   * @param data The data for the request.
+   * @param data.section
+   * @param data.option
+   * @param data.accept
+   * @returns Config Successful Response
+   * @throws ApiError
+   */
+  public static getConfigValue(
+    data: GetConfigValueData,
+  ): CancelablePromise<GetConfigValueResponse> {
+    return __request(OpenAPI, {
+      method: "GET",
+      url: "/public/config/section/{section}/option/{option}",
+      path: {
+        section: data.section,
+        option: data.option,
+      },
+      headers: {
+        accept: data.accept,
+      },
+      errors: {
+        401: "Unauthorized",
+        403: "Forbidden",
+        404: "Not Found",
+        406: "Not Acceptable",
         422: "Validation Error",
       },
     });
@@ -579,80 +633,60 @@ export class DagsService {
   }
 }
 
-export class ConfigService {
-  /**
-   * Get Configs
-   * Get configs for UI.
-   * @returns ConfigResponse Successful Response
-   * @throws ApiError
-   */
-  public static getConfigs(): CancelablePromise<GetConfigsResponse> {
-    return __request(OpenAPI, {
-      method: "GET",
-      url: "/ui/config",
-      errors: {
-        404: "Not Found",
-      },
-    });
-  }
-
+export class DashboardService {
   /**
-   * Get Config
+   * Historical Metrics
+   * Return cluster activity historical metrics.
    * @param data The data for the request.
-   * @param data.section
-   * @param data.accept
-   * @returns Config Successful Response
+   * @param data.startDate
+   * @param data.endDate
+   * @returns HistoricalMetricDataResponse Successful Response
    * @throws ApiError
    */
-  public static getConfig(
-    data: GetConfigData = {},
-  ): CancelablePromise<GetConfigResponse> {
+  public static historicalMetrics(
+    data: HistoricalMetricsData,
+  ): CancelablePromise<HistoricalMetricsResponse> {
     return __request(OpenAPI, {
       method: "GET",
-      url: "/public/config",
-      headers: {
-        accept: data.accept,
-      },
+      url: "/ui/dashboard/historical_metrics_data",
       query: {
-        section: data.section,
+        start_date: data.startDate,
+        end_date: data.endDate,
       },
       errors: {
-        401: "Unauthorized",
-        403: "Forbidden",
-        404: "Not Found",
-        406: "Not Acceptable",
+        400: "Bad Request",
         422: "Validation Error",
       },
     });
   }
+}
 
+export class GraphService {
   /**
-   * Get Config Value
+   * Graph Data
+   * Get Graph Data.
    * @param data The data for the request.
-   * @param data.section
-   * @param data.option
-   * @param data.accept
-   * @returns Config Successful Response
+   * @param data.dagId
+   * @param data.root
+   * @param data.includeUpstream
+   * @param data.includeDownstream
+   * @returns GraphDataResponse Successful Response
    * @throws ApiError
    */
-  public static getConfigValue(
-    data: GetConfigValueData,
-  ): CancelablePromise<GetConfigValueResponse> {
+  public static graphData(
+    data: GraphDataData,
+  ): CancelablePromise<GraphDataResponse2> {
     return __request(OpenAPI, {
       method: "GET",
-      url: "/public/config/section/{section}/option/{option}",
-      path: {
-        section: data.section,
-        option: data.option,
-      },
-      headers: {
-        accept: data.accept,
+      url: "/ui/graph/graph_data",
+      query: {
+        dag_id: data.dagId,
+        root: data.root,
+        include_upstream: data.includeUpstream,
+        include_downstream: data.includeDownstream,
       },
       errors: {
-        401: "Unauthorized",
-        403: "Forbidden",
-        404: "Not Found",
-        406: "Not Acceptable",
+        400: "Bad Request",
         422: "Validation Error",
       },
     });
diff --git a/airflow/ui/openapi-gen/requests/types.gen.ts 
b/airflow/ui/openapi-gen/requests/types.gen.ts
index 1adcdc8eba8..1f3c450625c 100644
--- a/airflow/ui/openapi-gen/requests/types.gen.ts
+++ b/airflow/ui/openapi-gen/requests/types.gen.ts
@@ -620,6 +620,16 @@ export type DagTagPydantic = {
  */
 export type DagWarningType = "asset conflict" | "non-existent pool";
 
+/**
+ * Edge serializer for responses.
+ */
+export type EdgeResponse = {
+  is_setup_teardown?: boolean | null;
+  label?: string | null;
+  source_id: string;
+  target_id: string;
+};
+
 /**
  * Event Log Collection Response.
  */
@@ -662,6 +672,17 @@ export type FastAPIAppResponse = {
   [key: string]: unknown | string;
 };
 
+/**
+ * Graph Data serializer for responses.
+ */
+export type GraphDataResponse = {
+  edges: Array<EdgeResponse>;
+  nodes: NodeResponse;
+  arrange: "BT" | "LR" | "RL" | "TB";
+};
+
+export type arrange = "BT" | "LR" | "RL" | "TB";
+
 /**
  * HTTPException Model used for error response.
  */
@@ -738,6 +759,30 @@ export type JobResponse = {
   unixname: string | null;
 };
 
+/**
+ * Node serializer for responses.
+ */
+export type NodeResponse = {
+  children?: Array<NodeResponse> | null;
+  id: string | null;
+  value: NodeValueResponse;
+};
+
+/**
+ * Graph Node Value responses.
+ */
+export type NodeValueResponse = {
+  isMapped?: boolean | null;
+  label?: string | null;
+  labelStyle?: string | null;
+  style?: string | null;
+  tooltip?: string | null;
+  rx: number;
+  ry: number;
+  clusterLabelPos?: string | null;
+  setupTeardownType?: "setup" | "teardown" | null;
+};
+
 /**
  * Request body for Clear Task Instances endpoint.
  */
@@ -1333,12 +1378,22 @@ export type DeleteDagAssetQueuedEventData = {
 
 export type DeleteDagAssetQueuedEventResponse = void;
 
-export type HistoricalMetricsData = {
-  endDate?: string | null;
-  startDate: string;
+export type GetConfigsResponse = ConfigResponse;
+
+export type GetConfigData = {
+  accept?: "application/json" | "text/plain" | "*/*";
+  section?: string | null;
 };
 
-export type HistoricalMetricsResponse = HistoricalMetricDataResponse;
+export type GetConfigResponse = Config;
+
+export type GetConfigValueData = {
+  accept?: "application/json" | "text/plain" | "*/*";
+  option: string;
+  section: string;
+};
+
+export type GetConfigValueResponse = Config;
 
 export type RecentDagRunsData = {
   dagDisplayNamePattern?: string | null;
@@ -1355,22 +1410,21 @@ export type RecentDagRunsData = {
 
 export type RecentDagRunsResponse = DAGWithLatestDagRunsCollectionResponse;
 
-export type GetConfigsResponse = ConfigResponse;
-
-export type GetConfigData = {
-  accept?: "application/json" | "text/plain" | "*/*";
-  section?: string | null;
+export type HistoricalMetricsData = {
+  endDate?: string | null;
+  startDate: string;
 };
 
-export type GetConfigResponse = Config;
+export type HistoricalMetricsResponse = HistoricalMetricDataResponse;
 
-export type GetConfigValueData = {
-  accept?: "application/json" | "text/plain" | "*/*";
-  option: string;
-  section: string;
+export type GraphDataData = {
+  dagId: string;
+  includeDownstream?: boolean;
+  includeUpstream?: boolean;
+  root?: string | null;
 };
 
-export type GetConfigValueResponse = Config;
+export type GraphDataResponse2 = GraphDataResponse;
 
 export type ListBackfillsData = {
   dagId: string;
@@ -2272,40 +2326,6 @@ export type $OpenApiTs = {
       };
     };
   };
-  "/ui/dashboard/historical_metrics_data": {
-    get: {
-      req: HistoricalMetricsData;
-      res: {
-        /**
-         * Successful Response
-         */
-        200: HistoricalMetricDataResponse;
-        /**
-         * Bad Request
-         */
-        400: HTTPExceptionResponse;
-        /**
-         * Validation Error
-         */
-        422: HTTPValidationError;
-      };
-    };
-  };
-  "/ui/dags/recent_dag_runs": {
-    get: {
-      req: RecentDagRunsData;
-      res: {
-        /**
-         * Successful Response
-         */
-        200: DAGWithLatestDagRunsCollectionResponse;
-        /**
-         * Validation Error
-         */
-        422: HTTPValidationError;
-      };
-    };
-  };
   "/ui/config": {
     get: {
       res: {
@@ -2382,6 +2402,59 @@ export type $OpenApiTs = {
       };
     };
   };
+  "/ui/dags/recent_dag_runs": {
+    get: {
+      req: RecentDagRunsData;
+      res: {
+        /**
+         * Successful Response
+         */
+        200: DAGWithLatestDagRunsCollectionResponse;
+        /**
+         * Validation Error
+         */
+        422: HTTPValidationError;
+      };
+    };
+  };
+  "/ui/dashboard/historical_metrics_data": {
+    get: {
+      req: HistoricalMetricsData;
+      res: {
+        /**
+         * Successful Response
+         */
+        200: HistoricalMetricDataResponse;
+        /**
+         * Bad Request
+         */
+        400: HTTPExceptionResponse;
+        /**
+         * Validation Error
+         */
+        422: HTTPValidationError;
+      };
+    };
+  };
+  "/ui/graph/graph_data": {
+    get: {
+      req: GraphDataData;
+      res: {
+        /**
+         * Successful Response
+         */
+        200: GraphDataResponse;
+        /**
+         * Bad Request
+         */
+        400: HTTPExceptionResponse;
+        /**
+         * Validation Error
+         */
+        422: HTTPValidationError;
+      };
+    };
+  };
   "/public/backfills": {
     get: {
       req: ListBackfillsData;
diff --git a/tests/api_fastapi/core_api/routes/ui/test_graph.py 
b/tests/api_fastapi/core_api/routes/ui/test_graph.py
new file mode 100644
index 00000000000..85dabd3ac6a
--- /dev/null
+++ b/tests/api_fastapi/core_api/routes/ui/test_graph.py
@@ -0,0 +1,198 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import pendulum
+import pytest
+
+from airflow.models import DagBag
+from airflow.operators.empty import EmptyOperator
+
+from tests_common.test_utils.db import clear_db_runs
+
+pytestmark = pytest.mark.db_test
+
+DAG_ID = "test_dag_id"
+
+
[email protected](autouse=True, scope="module")
+def examples_dag_bag():
+    # Speed up: We don't want example dags for this module
+
+    return DagBag(include_examples=False, read_dags_from_db=True)
+
+
[email protected](autouse=True)
+def clean():
+    clear_db_runs()
+    yield
+    clear_db_runs()
+
+
[email protected]
+def make_dag(dag_maker, session, time_machine):
+    with dag_maker(
+        dag_id=DAG_ID,
+        serialized=True,
+        session=session,
+        start_date=pendulum.DateTime(2023, 2, 1, 0, 0, 0, tzinfo=pendulum.UTC),
+    ):
+        EmptyOperator(task_id="task_1") >> EmptyOperator(task_id="task_2")
+
+    dag_maker.dagbag.sync_to_db()
+
+
+class TestGraphDataEndpoint:
+    @pytest.mark.parametrize(
+        "params, expected",
+        [
+            (
+                {"dag_id": DAG_ID},
+                {
+                    "arrange": "LR",
+                    "edges": [
+                        {
+                            "is_setup_teardown": None,
+                            "label": None,
+                            "source_id": "task_1",
+                            "target_id": "task_2",
+                        },
+                    ],
+                    "nodes": {
+                        "children": [
+                            {
+                                "children": None,
+                                "id": "task_1",
+                                "value": {
+                                    "clusterLabelPos": None,
+                                    "isMapped": None,
+                                    "label": "task_1",
+                                    "labelStyle": "fill:#000;",
+                                    "rx": 5,
+                                    "ry": 5,
+                                    "setupTeardownType": None,
+                                    "style": "fill:#e8f7e4;",
+                                    "tooltip": None,
+                                },
+                            },
+                            {
+                                "children": None,
+                                "id": "task_2",
+                                "value": {
+                                    "clusterLabelPos": None,
+                                    "isMapped": None,
+                                    "label": "task_2",
+                                    "labelStyle": "fill:#000;",
+                                    "rx": 5,
+                                    "ry": 5,
+                                    "setupTeardownType": None,
+                                    "style": "fill:#e8f7e4;",
+                                    "tooltip": None,
+                                },
+                            },
+                        ],
+                        "id": None,
+                        "value": {
+                            "clusterLabelPos": "top",
+                            "isMapped": False,
+                            "label": None,
+                            "labelStyle": "fill:#000;",
+                            "rx": 5,
+                            "ry": 5,
+                            "setupTeardownType": None,
+                            "style": "fill:CornflowerBlue",
+                            "tooltip": "",
+                        },
+                    },
+                },
+            ),
+            (
+                {
+                    "dag_id": DAG_ID,
+                    "root": "unknown_task",
+                },
+                {
+                    "arrange": "LR",
+                    "edges": [],
+                    "nodes": {
+                        "children": [],
+                        "id": None,
+                        "value": {
+                            "clusterLabelPos": "top",
+                            "isMapped": False,
+                            "label": None,
+                            "labelStyle": "fill:#000;",
+                            "rx": 5,
+                            "ry": 5,
+                            "setupTeardownType": None,
+                            "style": "fill:CornflowerBlue",
+                            "tooltip": "",
+                        },
+                    },
+                },
+            ),
+            (
+                {
+                    "dag_id": DAG_ID,
+                    "root": "task_1",
+                    "filter_upstream": False,
+                    "filter_downstream": False,
+                },
+                {
+                    "arrange": "LR",
+                    "edges": [],
+                    "nodes": {
+                        "children": [
+                            {
+                                "children": None,
+                                "id": "task_1",
+                                "value": {
+                                    "clusterLabelPos": None,
+                                    "isMapped": None,
+                                    "label": "task_1",
+                                    "labelStyle": "fill:#000;",
+                                    "rx": 5,
+                                    "ry": 5,
+                                    "setupTeardownType": None,
+                                    "style": "fill:#e8f7e4;",
+                                    "tooltip": None,
+                                },
+                            },
+                        ],
+                        "id": None,
+                        "value": {
+                            "clusterLabelPos": "top",
+                            "isMapped": False,
+                            "label": None,
+                            "labelStyle": "fill:#000;",
+                            "rx": 5,
+                            "ry": 5,
+                            "setupTeardownType": None,
+                            "style": "fill:CornflowerBlue",
+                            "tooltip": "",
+                        },
+                    },
+                },
+            ),
+        ],
+    )
+    @pytest.mark.usefixtures("make_dag")
+    def test_historical_metrics_data(self, test_client, params, expected):
+        response = test_client.get("/ui/graph/graph_data", params=params)
+        assert response.status_code == 200
+        assert response.json() == expected

Reply via email to