This is an automated email from the ASF dual-hosted git repository.
shahar1 pushed a commit to branch airflow-ctl/v0-1-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/airflow-ctl/v0-1-test by this
push:
new a40be82a7d8 [airflow-ctl/v0-1-test] Fix Dag detail page 500 for Dags
without a fileloc #69035 (#69110) (#76074)
a40be82a7d8 is described below
commit a40be82a7d8a1f9273314e50038d44e5142229f6
Author: Shahar Epstein <[email protected]>
AuthorDate: Fri Oct 30 20:28:12 2026 +0200
[airflow-ctl/v0-1-test] Fix Dag detail page 500 for Dags without a fileloc
#69035 (#69110) (#76074)
Only the fileloc datamodel change, its UI handling and tests are
backported, so airflowctl 0.1.x can read Dags that Airflow 3.4 servers return
with a null fileloc. The spec, _private_ui.yaml, UI openapi-gen and airflowctl
datamodels were regenerated with this branch's generators. Adds an airflowctl
test for a null fileloc.
(cherry picked from commit 7790bb52994a69bdcfd3bcaf4cdbd846797eaf6c)
Co-authored-by: Haseeb Malik
<[email protected]>
---
.../api_fastapi/core_api/datamodels/dags.py | 2 +-
.../api_fastapi/core_api/openapi/_private_ui.yaml | 4 +-
.../core_api/openapi/v2-rest-api-generated.yaml | 8 +++-
.../airflow/ui/openapi-gen/requests/schemas.gen.ts | 27 +++++++++++--
.../airflow/ui/openapi-gen/requests/types.gen.ts | 6 +--
.../ui/src/pages/Dag/Code/FileLocation.test.tsx | 47 ++++++++++++++++++++++
.../airflow/ui/src/pages/Dag/Code/FileLocation.tsx | 14 +++++--
.../core_api/routes/public/test_dags.py | 20 +++++++++
.../src/airflowctl/api/datamodels/generated.py | 4 +-
.../tests/airflow_ctl/api/test_operations.py | 12 ++++++
10 files changed, 128 insertions(+), 16 deletions(-)
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 ea73f1fca13..6bd9f4b5ebb 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
@@ -84,7 +84,7 @@ class DAGResponse(BaseModel):
bundle_name: str | None
bundle_version: str | None
relative_fileloc: str | None
- fileloc: str
+ fileloc: str | None
description: str | None
timetable_summary: str | None
timetable_description: str | None
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
index 3ae655b924e..598b5e94ffb 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
+++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
@@ -1871,7 +1871,9 @@ components:
- type: 'null'
title: Relative Fileloc
fileloc:
- type: string
+ anyOf:
+ - type: string
+ - type: 'null'
title: Fileloc
description:
anyOf:
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
index c58274f46c7..f0875f305bb 100644
---
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
+++
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
@@ -10199,7 +10199,9 @@ components:
- type: 'null'
title: Relative Fileloc
fileloc:
- type: string
+ anyOf:
+ - type: string
+ - type: 'null'
title: Fileloc
description:
anyOf:
@@ -10487,7 +10489,9 @@ components:
- type: 'null'
title: Relative Fileloc
fileloc:
- type: string
+ anyOf:
+ - type: string
+ - type: 'null'
title: Fileloc
description:
anyOf:
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 ee7206e2b0b..0c7ee2e5f1d 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
@@ -1850,7 +1850,14 @@ export const $DAGDetailsResponse = {
title: 'Relative Fileloc'
},
fileloc: {
- type: 'string',
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
title: 'Fileloc'
},
description: {
@@ -2291,7 +2298,14 @@ export const $DAGResponse = {
title: 'Relative Fileloc'
},
fileloc: {
- type: 'string',
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
title: 'Fileloc'
},
description: {
@@ -7736,7 +7750,14 @@ export const $DAGWithLatestDagRunsResponse = {
title: 'Relative Fileloc'
},
fileloc: {
- type: 'string',
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
title: 'Fileloc'
},
description: {
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 16a3d59b86b..2feb9ab4566 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
@@ -536,7 +536,7 @@ export type DAGDetailsResponse = {
bundle_name: string | null;
bundle_version: string | null;
relative_fileloc: string | null;
- fileloc: string;
+ fileloc: string | null;
description: string | null;
timetable_summary: string | null;
timetable_description: string | null;
@@ -615,7 +615,7 @@ export type DAGResponse = {
bundle_name: string | null;
bundle_version: string | null;
relative_fileloc: string | null;
- fileloc: string;
+ fileloc: string | null;
description: string | null;
timetable_summary: string | null;
timetable_description: string | null;
@@ -1897,7 +1897,7 @@ export type DAGWithLatestDagRunsResponse = {
bundle_name: string | null;
bundle_version: string | null;
relative_fileloc: string | null;
- fileloc: string;
+ fileloc: string | null;
description: string | null;
timetable_summary: string | null;
timetable_description: string | null;
diff --git
a/airflow-core/src/airflow/ui/src/pages/Dag/Code/FileLocation.test.tsx
b/airflow-core/src/airflow/ui/src/pages/Dag/Code/FileLocation.test.tsx
new file mode 100644
index 00000000000..10330576f98
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Code/FileLocation.test.tsx
@@ -0,0 +1,47 @@
+/*!
+ * 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.
+ */
+import "@testing-library/jest-dom";
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+
+import { Wrapper } from "src/utils/Wrapper";
+
+import { FileLocation } from "./FileLocation";
+
+describe("FileLocation", () => {
+ it("falls back to relativeFileloc when fileloc is null", () => {
+ render(
+ <Wrapper>
+ <FileLocation fileloc={null} relativeFileloc="example_dag.py" />
+ </Wrapper>,
+ );
+
+ expect(screen.getByText("example_dag.py")).toBeInTheDocument();
+ });
+
+ it("renders nothing when both fileloc and relativeFileloc are null", () => {
+ const { container } = render(
+ <Wrapper>
+ <FileLocation fileloc={null} relativeFileloc={null} />
+ </Wrapper>,
+ );
+
+ expect(container).toBeEmptyDOMElement();
+ });
+});
diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Code/FileLocation.tsx
b/airflow-core/src/airflow/ui/src/pages/Dag/Code/FileLocation.tsx
index 34c14d0c6b4..dabfb4b8104 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Code/FileLocation.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Code/FileLocation.tsx
@@ -21,15 +21,21 @@ import { Box, Code, HStack } from "@chakra-ui/react";
import { ClipboardIconButton, ClipboardRoot, Tooltip } from
"src/components/ui";
type FileLocationProps = {
- readonly fileloc: string;
+ readonly fileloc: string | null;
readonly relativeFileloc: string | null;
};
export const FileLocation = ({ fileloc, relativeFileloc }: FileLocationProps)
=> {
+ // A Dag with run history but no parsed source file has no fileloc to show.
+ if ((fileloc === null || fileloc === "") && (relativeFileloc === null ||
relativeFileloc === "")) {
+ return undefined;
+ }
+
+ const fullPath = fileloc ?? relativeFileloc ?? "";
const displayFilename =
relativeFileloc !== null && relativeFileloc !== ""
? relativeFileloc
- : (fileloc.split("/").at(-1) ?? fileloc);
+ : (fileloc?.split("/").at(-1) ?? fileloc ?? "");
return (
<Box
@@ -41,12 +47,12 @@ export const FileLocation = ({ fileloc, relativeFileloc }:
FileLocationProps) =>
py={1}
>
<HStack gap={2}>
- <Tooltip closeDelay={100} content={fileloc} openDelay={100}>
+ <Tooltip closeDelay={100} content={fullPath} openDelay={100}>
<Code fontSize="13px" wordBreak="break-word">
{displayFilename}
</Code>
</Tooltip>
- <ClipboardRoot value={fileloc}>
+ <ClipboardRoot value={fullPath}>
<ClipboardIconButton size="2xs" />
</ClipboardRoot>
</HStack>
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 55515b76931..5389cf94bd7 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
@@ -1292,6 +1292,26 @@ class TestGetDag(TestDagEndpoint):
assert response.status_code == 403
+class TestDagWithoutFileloc(TestDagEndpoint):
+ def _make_dag_without_fileloc(self, dag_maker, session,
dag_id="test_dag_no_fileloc"):
+ with dag_maker(dag_id=dag_id, schedule=None):
+ EmptyOperator(task_id="task1")
+ dag_maker.create_dagrun(state=DagRunState.SUCCESS)
+ dag_maker.sync_dagbag_to_db()
+ dag_model = session.get(DagModel, dag_id)
+ dag_model.fileloc = None
+ dag_model.relative_fileloc = None
+ session.commit()
+ return dag_id
+
+ @pytest.mark.parametrize("path_suffix", ["", "/details"])
+ def test_detail_endpoints_serve_dag_with_null_fileloc(self, session,
test_client, dag_maker, path_suffix):
+ dag_id = self._make_dag_without_fileloc(dag_maker, session)
+ response = test_client.get(f"/dags/{dag_id}{path_suffix}")
+ assert response.status_code == 200
+ assert response.json()["fileloc"] is None
+
+
class TestDeleteDAG(TestDagEndpoint):
"""Unit tests for Delete DAG."""
diff --git a/airflow-ctl/src/airflowctl/api/datamodels/generated.py
b/airflow-ctl/src/airflowctl/api/datamodels/generated.py
index 0c418d238ba..b8f1e473dad 100644
--- a/airflow-ctl/src/airflowctl/api/datamodels/generated.py
+++ b/airflow-ctl/src/airflowctl/api/datamodels/generated.py
@@ -1351,7 +1351,7 @@ class DAGDetailsResponse(BaseModel):
bundle_name: Annotated[str | None, Field(title="Bundle Name")] = None
bundle_version: Annotated[str | None, Field(title="Bundle Version")] = None
relative_fileloc: Annotated[str | None, Field(title="Relative Fileloc")] =
None
- fileloc: Annotated[str, Field(title="Fileloc")]
+ fileloc: Annotated[str | None, Field(title="Fileloc")] = None
description: Annotated[str | None, Field(title="Description")] = None
timetable_summary: Annotated[str | None, Field(title="Timetable Summary")]
= None
timetable_description: Annotated[str | None, Field(title="Timetable
Description")] = None
@@ -1416,7 +1416,7 @@ class DAGResponse(BaseModel):
bundle_name: Annotated[str | None, Field(title="Bundle Name")] = None
bundle_version: Annotated[str | None, Field(title="Bundle Version")] = None
relative_fileloc: Annotated[str | None, Field(title="Relative Fileloc")] =
None
- fileloc: Annotated[str, Field(title="Fileloc")]
+ fileloc: Annotated[str | None, Field(title="Fileloc")] = None
description: Annotated[str | None, Field(title="Description")] = None
timetable_summary: Annotated[str | None, Field(title="Timetable Summary")]
= None
timetable_description: Annotated[str | None, Field(title="Timetable
Description")] = None
diff --git a/airflow-ctl/tests/airflow_ctl/api/test_operations.py
b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
index 56f4567718c..80c8173bf52 100644
--- a/airflow-ctl/tests/airflow_ctl/api/test_operations.py
+++ b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
@@ -1255,6 +1255,18 @@ class TestDagOperations:
response = client.dags.get("dag_id")
assert response == self.dag_response
+ def test_get_accepts_null_fileloc(self):
+ """Servers with #69110 return ``fileloc: null`` for a Dag whose file
location is unknown."""
+ payload = json.loads(self.dag_response.model_dump_json())
+ payload["fileloc"] = None
+
+ def handle_request(request: httpx.Request) -> httpx.Response:
+ return httpx.Response(200, json=payload)
+
+ client = make_api_client(transport=httpx.MockTransport(handle_request))
+ response = client.dags.get("dag_id")
+ assert response.fileloc is None
+
def test_get_details(self):
def handle_request(request: httpx.Request) -> httpx.Response:
assert request.url.path == "/api/v2/dags/dag_id/details"