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 31c9d24fa1659..2151a8599ccd8 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
@@ -91,7 +91,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 618b158c0f418..45da42f9224a4 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
@@ -2345,7 +2345,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 44f797e1d3c36..571d9cbeb9d6a 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
@@ -13343,7 +13343,9 @@ components:
- type: 'null'
title: Relative Fileloc
fileloc:
- type: string
+ anyOf:
+ - type: string
+ - type: 'null'
title: Fileloc
description:
anyOf:
@@ -13650,7 +13652,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 ebf27dbbb84ea..08dbfa80a44c0 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
@@ -2800,7 +2800,14 @@ export const $DAGDetailsResponse = {
title: 'Relative Fileloc'
},
fileloc: {
- type: 'string',
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
title: 'Fileloc'
},
description: {
@@ -3277,7 +3284,14 @@ export const $DAGResponse = {
title: 'Relative Fileloc'
},
fileloc: {
- type: 'string',
+ anyOf: [
+ {
+ type: 'string'
+ },
+ {
+ type: 'null'
+ }
+ ],
title: 'Fileloc'
},
description: {
@@ -9016,7 +9030,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 c070c9d74d71f..a9943390b27c3 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
@@ -837,7 +837,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;
@@ -920,7 +920,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;
@@ -2303,7 +2303,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 0000000000000..10330576f9832
--- /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(
+
+
+ ,
+ );
+
+ expect(screen.getByText("example_dag.py")).toBeInTheDocument();
+ });
+
+ it("renders nothing when both fileloc and relativeFileloc are null", () => {
+ const { container } = render(
+
+
+ ,
+ );
+
+ 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 34c14d0c6b434..dabfb4b81045a 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 (
py={1}
>
-
+
{displayFilename}
-
+
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 fd01b167263de..4831394c45bf1 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
@@ -1392,6 +1392,26 @@ def test_get_dag_should_response_403(self, unauthorized_test_client):
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 7fbe5a86d2e0b..fa352e611408f 100644
--- a/airflow-ctl/src/airflowctl/api/datamodels/generated.py
+++ b/airflow-ctl/src/airflowctl/api/datamodels/generated.py
@@ -1764,7 +1764,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
@@ -2557,7 +2557,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