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