From 83775c99ee9735823fe9fd9d589b48351164f2eb Mon Sep 17 00:00:00 2001 From: Yeonguk Date: Tue, 13 May 2025 18:20:49 +0900 Subject: [PATCH 1/9] feat: Add owner_links field to DAGDetailsResponse model and update related schemas --- .../api_fastapi/core_api/datamodels/dags.py | 1 + .../core_api/openapi/v1-rest-api-generated.yaml | 7 +++++++ .../airflow/ui/openapi-gen/requests/schemas.gen.ts | 14 ++++++++++++++ .../airflow/ui/openapi-gen/requests/types.gen.ts | 3 +++ .../core_api/routes/public/test_dags.py | 1 + .../src/airflowctl/api/datamodels/generated.py | 1 + 6 files changed, 27 insertions(+) diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dags.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dags.py index c3d8dbd7e4fad..f17b3d943f99c 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dags.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dags.py @@ -156,6 +156,7 @@ class DAGDetailsResponse(DAGResponse): timezone: str | None last_parsed: datetime | None default_args: abc.Mapping | None + owner_links: dict[str, str] | None = None @field_validator("timezone", mode="before") @classmethod diff --git a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v1-rest-api-generated.yaml b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v1-rest-api-generated.yaml index 1cb0a8bfdbab1..18527e59fb050 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v1-rest-api-generated.yaml +++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v1-rest-api-generated.yaml @@ -8117,6 +8117,13 @@ components: type: object - type: 'null' title: Default Args + owner_links: + anyOf: + - additionalProperties: + type: string + type: object + - type: 'null' + title: Owner Links file_token: type: string title: File Token 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 7a7e00af68ecd..8b3913a40844f 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 @@ -1742,6 +1742,20 @@ export const $DAGDetailsResponse = { ], title: "Default Args", }, + owner_links: { + anyOf: [ + { + additionalProperties: { + type: "string", + }, + type: "object", + }, + { + type: "null", + }, + ], + title: "Owner Links", + }, file_token: { type: "string", title: "File Token", 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 294f9f0b02d41..f14fc0228c626 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 @@ -476,6 +476,9 @@ export type DAGDetailsResponse = { default_args: { [key: string]: unknown; } | null; + owner_links?: { + [key: string]: string; + } | null; /** * Return file token. */ diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py index a8fa987a84ece..52c0b73e3bc81 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py @@ -461,6 +461,7 @@ def test_dag_details( "next_dagrun_logical_date": None, "next_dagrun_run_after": None, "owners": ["airflow"], + "owner_links": {"airflow": "https://airflow.apache.org"}, "params": { "foo": { "__class": "airflow.sdk.definitions.param.Param", diff --git a/airflow-ctl/src/airflowctl/api/datamodels/generated.py b/airflow-ctl/src/airflowctl/api/datamodels/generated.py index 6706a28740dad..96bdde28b5724 100644 --- a/airflow-ctl/src/airflowctl/api/datamodels/generated.py +++ b/airflow-ctl/src/airflowctl/api/datamodels/generated.py @@ -1169,6 +1169,7 @@ class DAGDetailsResponse(BaseModel): timezone: Annotated[str | None, Field(title="Timezone")] = None last_parsed: Annotated[datetime | None, Field(title="Last Parsed")] = None default_args: Annotated[dict[str, Any] | None, Field(title="Default Args")] = None + owner_links: Annotated[dict[str, str] | None, Field(title="Owner Links")] = None file_token: Annotated[str, Field(description="Return file token.", title="File Token")] concurrency: Annotated[ int, Field(description="Return max_active_tasks as concurrency.", title="Concurrency") From ae051a24626725ca41d27c047095fb23a70550ea Mon Sep 17 00:00:00 2001 From: Yeonguk Date: Tue, 13 May 2025 18:23:51 +0900 Subject: [PATCH 2/9] feat: add owner_links to DAG Header owner section --- .../src/airflow/ui/src/pages/Dag/Header.tsx | 3 +- .../ui/src/pages/DagsList/DagOwners.tsx | 55 +++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 airflow-core/src/airflow/ui/src/pages/DagsList/DagOwners.tsx diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx index 94874dd8aea77..c2e5fec62a0e9 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx @@ -28,6 +28,7 @@ import DisplayMarkdownButton from "src/components/DisplayMarkdownButton"; import { HeaderCard } from "src/components/HeaderCard"; import { TogglePause } from "src/components/TogglePause"; +import { DagOwners } from "../DagsList/DagOwners"; import { DagTags } from "../DagsList/DagTags"; import { Schedule } from "../DagsList/Schedule"; @@ -73,7 +74,7 @@ export const Header = ({ }, { label: "Owner", - value: dag?.owners.join(", "), + value: , }, { label: "Tags", diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagOwners.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagOwners.tsx new file mode 100644 index 0000000000000..8c1b80c9dc89b --- /dev/null +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagOwners.tsx @@ -0,0 +1,55 @@ +/*! + * 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. + */ +const DEFAULT_OWNERS: Array = []; + +export const DagOwners = ({ + ownerLinks, + owners = DEFAULT_OWNERS, +}: { + readonly ownerLinks?: Record | null; + readonly owners?: Array; +}) => { + const hasOwnerLinks = ownerLinks && Object.keys(ownerLinks).length > 0; + + if (!hasOwnerLinks) { + return {owners.join(", ")}; + } + + return ( + <> + {owners.map((owner) => { + const trimmedOwner = owner.trim(); + const link = ownerLinks[trimmedOwner]; + const href = link ?? `?search=${encodeURIComponent(trimmedOwner)}`; + + return ( + + {trimmedOwner} + + ); + })} + + ); +}; From 2fdaa8104fbc9005202d0dc1ecc2316044ca2360 Mon Sep 17 00:00:00 2001 From: Yeonguk Date: Wed, 14 May 2025 03:44:30 +0900 Subject: [PATCH 3/9] remove owner_links in test code --- .../tests/unit/api_fastapi/core_api/routes/public/test_dags.py | 1 - 1 file changed, 1 deletion(-) diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py index 52c0b73e3bc81..a8fa987a84ece 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py @@ -461,7 +461,6 @@ def test_dag_details( "next_dagrun_logical_date": None, "next_dagrun_run_after": None, "owners": ["airflow"], - "owner_links": {"airflow": "https://airflow.apache.org"}, "params": { "foo": { "__class": "airflow.sdk.definitions.param.Param", From af841c83c47f78a6770ada0f91be8087828f9705 Mon Sep 17 00:00:00 2001 From: Yeonguk Date: Wed, 14 May 2025 04:35:26 +0900 Subject: [PATCH 4/9] add tests for DAG owner_links in DAG details endpoint --- .../unit/api_fastapi/core_api/routes/public/test_dags.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py index a8fa987a84ece..11d06dac89989 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py @@ -396,15 +396,15 @@ class TestDagDetails(TestDagEndpoint): """Unit tests for DAG Details.""" @pytest.mark.parametrize( - "query_params, dag_id, expected_status_code, dag_display_name, start_date", + "query_params, dag_id, expected_status_code, dag_display_name, start_date, owner_links", [ - ({}, "fake_dag_id", 404, "fake_dag", "2023-12-31T00:00:00Z"), - ({}, DAG2_ID, 200, DAG2_ID, "2021-06-15T00:00:00Z"), + ({}, "fake_dag_id", 404, "fake_dag", "2023-12-31T00:00:00Z", {}), + ({}, DAG2_ID, 200, DAG2_ID, "2021-06-15T00:00:00Z", {}), ], ) @pytest.mark.usefixtures("configure_git_connection_for_dag_bundle") def test_dag_details( - self, test_client, query_params, dag_id, expected_status_code, dag_display_name, start_date + self, test_client, query_params, dag_id, expected_status_code, dag_display_name, start_date, owner_links ): response = test_client.get(f"/dags/{dag_id}/details", params=query_params) assert response.status_code == expected_status_code @@ -461,6 +461,7 @@ def test_dag_details( "next_dagrun_logical_date": None, "next_dagrun_run_after": None, "owners": ["airflow"], + "owner_links": {}, "params": { "foo": { "__class": "airflow.sdk.definitions.param.Param", From 2c00444245ec76ad723be03982455f5d3d22c9fe Mon Sep 17 00:00:00 2001 From: Yeonguk Date: Wed, 14 May 2025 07:33:09 +0900 Subject: [PATCH 5/9] refactor: apply ruff formatting to test_dags.py --- .../unit/api_fastapi/core_api/routes/public/test_dags.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py index 11d06dac89989..64f0758512358 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py @@ -404,7 +404,14 @@ class TestDagDetails(TestDagEndpoint): ) @pytest.mark.usefixtures("configure_git_connection_for_dag_bundle") def test_dag_details( - self, test_client, query_params, dag_id, expected_status_code, dag_display_name, start_date, owner_links + self, + test_client, + query_params, + dag_id, + expected_status_code, + dag_display_name, + start_date, + owner_links, ): response = test_client.get(f"/dags/{dag_id}/details", params=query_params) assert response.status_code == expected_status_code From b505a9f510c9a4e15ab442c0e082137e964b2043 Mon Sep 17 00:00:00 2001 From: Yeonguk Date: Fri, 16 May 2025 02:56:33 +0900 Subject: [PATCH 6/9] refactor: simplify DagOwners component --- .../ui/src/pages/DagsList/DagOwners.tsx | 47 ++++++++----------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagOwners.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagOwners.tsx index 8c1b80c9dc89b..86c36a0da5c27 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagOwners.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagOwners.tsx @@ -16,6 +16,8 @@ * specific language governing permissions and limitations * under the License. */ +import { Link, Text } from "@chakra-ui/react"; + const DEFAULT_OWNERS: Array = []; export const DagOwners = ({ @@ -24,32 +26,21 @@ export const DagOwners = ({ }: { readonly ownerLinks?: Record | null; readonly owners?: Array; -}) => { - const hasOwnerLinks = ownerLinks && Object.keys(ownerLinks).length > 0; - - if (!hasOwnerLinks) { - return {owners.join(", ")}; - } - - return ( - <> - {owners.map((owner) => { - const trimmedOwner = owner.trim(); - const link = ownerLinks[trimmedOwner]; - const href = link ?? `?search=${encodeURIComponent(trimmedOwner)}`; +}) => ( + <> + {owners.map((owner) => { + const link = ownerLinks?.[owner]; + const hasOwnerLinks = link !== undefined; - return ( - - {trimmedOwner} - - ); - })} - - ); -}; + return hasOwnerLinks ? ( + + {owner} + + ) : ( + + {owner} + + ); + })} + +); From 882e5a6c6996c8bcf11139289d72263d386f35c9 Mon Sep 17 00:00:00 2001 From: Yeonguk Date: Fri, 16 May 2025 03:59:02 +0900 Subject: [PATCH 7/9] refactor: improve separator logic in DagOwners component --- .../ui/src/pages/DagsList/DagOwners.tsx | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagOwners.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagOwners.tsx index 86c36a0da5c27..a9367a9a34b8d 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagOwners.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagOwners.tsx @@ -17,6 +17,7 @@ * under the License. */ import { Link, Text } from "@chakra-ui/react"; +import React from "react"; const DEFAULT_OWNERS: Array = []; @@ -28,18 +29,28 @@ export const DagOwners = ({ readonly owners?: Array; }) => ( <> - {owners.map((owner) => { + {owners.map((owner, index) => { const link = ownerLinks?.[owner]; - const hasOwnerLinks = link !== undefined; + const hasOwnerLink = link !== undefined; + const isLast = index === owners.length - 1; - return hasOwnerLinks ? ( - - {owner} - - ) : ( - - {owner} - + return ( + + {hasOwnerLink ? ( + + {owner} + + ) : ( + + {owner} + + )} + {!isLast && ( + + {",\u00A0"} + + )} + ); })} From 46a14870f2c9026d621fe21f492540068cfb2304 Mon Sep 17 00:00:00 2001 From: Yeonguk Date: Fri, 16 May 2025 10:45:57 +0900 Subject: [PATCH 8/9] refactor: apply LimitedItemsList for cleaner item rendering logic --- .../ui/src/pages/DagsList/DagOwners.tsx | 48 ++++++++----------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagOwners.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagOwners.tsx index a9367a9a34b8d..6ef0ffa723bc1 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagOwners.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagOwners.tsx @@ -17,9 +17,11 @@ * under the License. */ import { Link, Text } from "@chakra-ui/react"; -import React from "react"; + +import { LimitedItemsList } from "src/components/LimitedItemsList"; const DEFAULT_OWNERS: Array = []; +const MAX_OWNERS = 3; export const DagOwners = ({ ownerLinks, @@ -27,31 +29,21 @@ export const DagOwners = ({ }: { readonly ownerLinks?: Record | null; readonly owners?: Array; -}) => ( - <> - {owners.map((owner, index) => { - const link = ownerLinks?.[owner]; - const hasOwnerLink = link !== undefined; - const isLast = index === owners.length - 1; +}) => { + const items = owners.map((owner) => { + const link = ownerLinks?.[owner]; + const hasOwnerLink = link !== undefined; + + return hasOwnerLink ? ( + + {owner} + + ) : ( + + {owner} + + ); + }); - return ( - - {hasOwnerLink ? ( - - {owner} - - ) : ( - - {owner} - - )} - {!isLast && ( - - {",\u00A0"} - - )} - - ); - })} - -); + return ; +}; From 22953cf2f888f6d1c4d80047370d9aa0ff145c25 Mon Sep 17 00:00:00 2001 From: Yeonguk Date: Sat, 17 May 2025 01:32:38 +0900 Subject: [PATCH 9/9] refactor: enhance Link component attributes --- .../src/airflow/ui/src/pages/DagsList/DagOwners.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagOwners.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagOwners.tsx index 6ef0ffa723bc1..1ba38be25105c 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagOwners.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagOwners.tsx @@ -35,7 +35,13 @@ export const DagOwners = ({ const hasOwnerLink = link !== undefined; return hasOwnerLink ? ( - + {owner} ) : (