From 36746654820fe5bcd4715dc6be753eda5d6a724c Mon Sep 17 00:00:00 2001 From: Shivam Rastogi <6463385+shivaam@users.noreply.github.com> Date: Fri, 10 Jul 2026 10:03:35 -0700 Subject: [PATCH 1/4] UI: Add timetable type filter to Dag list Users could filter by timetable type through the public API, but the Dag list uses the private UI endpoint. This makes the same capability available from the page users actually use. --- .../core_api/openapi/_private_ui.yaml | 8 ++ .../api_fastapi/core_api/routes/ui/dags.py | 5 ++ .../ui/openapi-gen/requests/services.gen.ts | 2 + .../ui/public/i18n/locales/en/dags.json | 4 +- .../airflow/ui/src/constants/searchParams.ts | 1 + .../src/airflow/ui/src/mocks/handlers/dags.ts | 14 ++- .../DagsList/DagsFilters/DagsFilters.test.tsx | 18 +++- .../DagsList/DagsFilters/DagsFilters.tsx | 14 +++ .../DagsFilters/TimetableTypeFilter.tsx | 89 +++++++++++++++++++ .../ui/src/pages/DagsList/DagsList.tsx | 3 + .../src/airflow/ui/src/queries/useDags.tsx | 3 + .../core_api/routes/ui/test_dags.py | 14 +++ 12 files changed, 170 insertions(+), 5 deletions(-) create mode 100644 airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TimetableTypeFilter.tsx 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 ca8f729998250..26a5a8e499ea7 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 @@ -551,6 +551,14 @@ paths: description: Filter Dags by asset dependency (name or URI) title: Asset Dependency description: Filter Dags by asset dependency (name or URI) + - name: timetable_type + in: query + required: false + schema: + type: array + items: + type: string + title: Timetable Type - name: has_pending_actions in: query required: false diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dags.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dags.py index 38421d387b10a..b46c0ea0cab84 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dags.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dags.py @@ -130,6 +130,10 @@ def get_dags( is_favorite: QueryFavoriteFilter, has_asset_schedule: QueryHasAssetScheduleFilter, asset_dependency: QueryAssetDependencyFilter, + timetable_type: Annotated[ + FilterParam[list[str] | None], + Depends(filter_param_factory(DagModel.timetable_type, list[str], FilterOptionEnum.IN)), + ], has_pending_actions: QueryPendingActionsFilter, readable_dags_filter: ReadableDagsFilterDep, session: SessionDep, @@ -165,6 +169,7 @@ def get_dags( is_favorite, has_asset_schedule, asset_dependency, + timetable_type, has_pending_actions, readable_dags_filter, bundle_name, diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts index b2e288a93f173..73b0a4c540746 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts @@ -1977,6 +1977,7 @@ export class DagService { * @param data.isFavorite * @param data.hasAssetSchedule Filter Dags with asset-based scheduling * @param data.assetDependency Filter Dags by asset dependency (name or URI) + * @param data.timetableType * @param data.hasPendingActions * @returns DAGWithLatestDagRunsCollectionResponse Successful Response * @throws ApiError @@ -2009,6 +2010,7 @@ export class DagService { is_favorite: data.isFavorite, has_asset_schedule: data.hasAssetSchedule, asset_dependency: data.assetDependency, + timetable_type: data.timetableType, has_pending_actions: data.hasPendingActions }, errors: { diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/dags.json b/airflow-core/src/airflow/ui/public/i18n/locales/en/dags.json index 78b8681b423a5..7cd6071762a6c 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/en/dags.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/dags.json @@ -22,7 +22,9 @@ "all": "All", "paused": "Paused" }, - "runIdPatternFilter": "Search Dag Runs" + "runIdPatternFilter": "Search Dag Runs", + "timetableType": "Timetable type", + "timetableTypeClear": "Clear timetable type filter" }, "ownerLink": "Owner link for {{owner}}", "runAndTaskActions": { diff --git a/airflow-core/src/airflow/ui/src/constants/searchParams.ts b/airflow-core/src/airflow/ui/src/constants/searchParams.ts index eee952f020786..cf1174b881e81 100644 --- a/airflow-core/src/airflow/ui/src/constants/searchParams.ts +++ b/airflow-core/src/airflow/ui/src/constants/searchParams.ts @@ -105,6 +105,7 @@ export enum SearchParamsKeys { TASK_ID_PATTERN = "task_id_pattern", TASK_STATE = "task_state", TEAMS = "teams", + TIMETABLE_TYPE = "timetable_type", TRIGGER_RULE = "trigger_rule", TRIGGERING_USER = "triggering_user", TRIGGERING_USER_NAME_PATTERN = "triggering_user_name_pattern", diff --git a/airflow-core/src/airflow/ui/src/mocks/handlers/dags.ts b/airflow-core/src/airflow/ui/src/mocks/handlers/dags.ts index b3d4d1fea35de..92f2fa7d627d0 100644 --- a/airflow-core/src/airflow/ui/src/mocks/handlers/dags.ts +++ b/airflow-core/src/airflow/ui/src/mocks/handlers/dags.ts @@ -49,6 +49,7 @@ const successDag = { pending_actions: [], tags: [{ dag_id: "tutorial_taskflow_api_success", name: "example" }], timetable_description: "Never, external triggers only", + timetable_type: "NullTimetable", }; const failedDag = { @@ -82,6 +83,7 @@ const failedDag = { pending_actions: [], tags: [{ dag_id: "tutorial_taskflow_api_failed", name: "example" }], timetable_description: "Never, external triggers only", + timetable_type: "CronTriggerTimetable", }; const pausedDag = { @@ -104,10 +106,15 @@ const pausedDag = { pending_actions: [], tags: [{ dag_id: "paused_dag", name: "example" }], timetable_description: "Never, external triggers only", + timetable_type: "NullTimetable", }; -const filterDagsByPaused = (paused: string | null) => { - const allDags = [successDag, failedDag, pausedDag]; +const filterDags = ({ paused, timetableTypes }: { paused: string | null; timetableTypes: Array }) => { + let allDags = [successDag, failedDag, pausedDag]; + + if (timetableTypes.length > 0) { + allDags = allDags.filter((dag) => timetableTypes.includes(dag.timetable_type)); + } if (paused === "true") { return allDags.filter((dag) => dag.is_paused); @@ -125,6 +132,7 @@ export const handlers: Array = [ const lastDagRunState = url.searchParams.get("last_dag_run_state"); const orderBy = url.searchParams.get("order_by"); const paused = url.searchParams.get("paused"); + const timetableTypes = url.searchParams.getAll("timetable_type"); if (lastDagRunState === "success") { return HttpResponse.json({ @@ -138,7 +146,7 @@ export const handlers: Array = [ }); } - let dags = filterDagsByPaused(paused); + let dags = filterDags({ paused, timetableTypes }); if (orderBy === "last_run_run_after" || orderBy === "-last_run_run_after") { dags = [failedDag, successDag, pausedDag].filter((dag) => dags.includes(dag)); diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.test.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.test.tsx index aac7b895ae4c8..714a9ef234bec 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.test.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.test.tsx @@ -17,7 +17,7 @@ * under the License. */ import "@testing-library/jest-dom"; -import { render, screen, waitFor } from "@testing-library/react"; +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { describe, it, expect, vi } from "vitest"; import { AppWrapper } from "src/utils/AppWrapper"; @@ -67,4 +67,20 @@ describe("Paused filter with hide_paused_dags_by_default enabled", () => { await waitFor(() => expect(screen.getByText("paused_dag")).toBeInTheDocument()); await waitFor(() => expect(screen.queryByText("tutorial_taskflow_api_success")).not.toBeInTheDocument()); }); + + it("filters dags by timetable type", async () => { + render(); + + await waitFor(() => expect(screen.getByText("tutorial_taskflow_api_success")).toBeInTheDocument()); + expect(screen.getByText("tutorial_taskflow_api_failed")).toBeInTheDocument(); + + fireEvent.change(screen.getByTestId("dags-timetable-type-filter"), { + target: { value: "CronTriggerTimetable" }, + }); + + await waitFor(() => { + expect(screen.queryByText("tutorial_taskflow_api_success")).not.toBeInTheDocument(); + expect(screen.getByText("tutorial_taskflow_api_failed")).toBeInTheDocument(); + }); + }); }); diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx index 424dbb1b6159d..5927e9951ee3f 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx @@ -34,6 +34,7 @@ import { RequiredActionFilter } from "./RequiredActionFilter"; import { RunStateSelect } from "./RunStateSelect"; import { TagFilter } from "./TagFilter"; import { TeamFilter } from "./TeamFilter"; +import { TimetableTypeFilter } from "./TimetableTypeFilter"; const { DAG_RUN_STATE: DAG_RUN_STATE_PARAM, @@ -43,6 +44,7 @@ const { OFFSET: OFFSET_PARAM, PAUSED: PAUSED_PARAM, TEAMS: TEAMS_PARAM, + TIMETABLE_TYPE: TIMETABLE_TYPE_PARAM, }: SearchParamsKeysType = SearchParamsKeys; type BooleanFilterValue = "all" | "false" | "true"; @@ -68,6 +70,7 @@ export const DagsFilters = () => { const state = searchParams.get(LAST_DAG_RUN_STATE_PARAM); const activeRunState = searchParams.get(DAG_RUN_STATE_PARAM); const selectedTeams = searchParams.getAll(TEAMS_PARAM); + const timetableType = searchParams.get(TIMETABLE_TYPE_PARAM) ?? ""; const [pattern, setPattern] = useState(""); @@ -141,6 +144,16 @@ export const DagsFilters = () => { setSearchParams(searchParams); }; + const handleTimetableTypeChange = (value: string) => { + if (value) { + searchParams.set(TIMETABLE_TYPE_PARAM, value); + } else { + searchParams.delete(TIMETABLE_TYPE_PARAM); + } + resetPagination(); + setSearchParams(searchParams); + }; + const handleSelectTagsChange = ( tags: MultiValue<{ label: string; @@ -184,6 +197,7 @@ export const DagsFilters = () => { /> + { void fetchNextPage(); diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TimetableTypeFilter.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TimetableTypeFilter.tsx new file mode 100644 index 0000000000000..06d325e4fc527 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TimetableTypeFilter.tsx @@ -0,0 +1,89 @@ +/*! + * 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 { CloseButton, Input, InputGroup } from "@chakra-ui/react"; +import { useEffect, useRef, useState, type ChangeEvent } from "react"; +import { useTranslation } from "react-i18next"; +import { FiClock } from "react-icons/fi"; +import { useDebouncedCallback } from "use-debounce"; + +const debounceDelay = 200; + +type Props = { + readonly onChange: (value: string) => void; + readonly value: string; +}; + +export const TimetableTypeFilter = ({ onChange, value: initialValue }: Props) => { + const [value, setValue] = useState(initialValue); + const lastSentValue = useRef(initialValue); + const { t: translate } = useTranslation("dags"); + + const handleFilterChange = useDebouncedCallback((newValue: string) => { + lastSentValue.current = newValue; + onChange(newValue); + }, debounceDelay); + + useEffect(() => { + if (initialValue !== lastSentValue.current) { + setValue(initialValue); + lastSentValue.current = initialValue; + } + }, [initialValue]); + + const onInputChange = (event: ChangeEvent) => { + const newValue = event.target.value; + + setValue(newValue); + handleFilterChange(newValue.trim()); + }; + + const clearFilter = () => { + handleFilterChange.cancel(); + lastSentValue.current = ""; + setValue(""); + onChange(""); + }; + + return ( + + ) : undefined + } + maxWidth="260px" + minWidth="220px" + startElement={} + > + + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx index f82a9e90c428a..f99ee95980a18 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx @@ -221,6 +221,7 @@ const { OWNERS, PAUSED, TEAMS, + TIMETABLE_TYPE, }: SearchParamsKeysType = SearchParamsKeys; const createCardDef = (runStateContext: RunStateCountsContext): CardDef => ({ @@ -259,6 +260,7 @@ export const DagsList = () => { const pendingReviews = searchParams.get(NEEDS_REVIEW); const owners = searchParams.getAll(OWNERS); const teams = searchParams.getAll(TEAMS); + const timetableType = searchParams.getAll(TIMETABLE_TYPE); const { setTableURLState, tableURLState } = useTableURLState(); @@ -323,6 +325,7 @@ export const DagsList = () => { tags: selectedTags, tagsMatchMode: selectedMatchMode, teams: teams.length > 0 ? teams : undefined, + timetableType: timetableType.length > 0 ? timetableType : undefined, }); const { data: runStateCountsData, isLoading: runStateCountsLoading } = useDagRunStateCounts({ diff --git a/airflow-core/src/airflow/ui/src/queries/useDags.tsx b/airflow-core/src/airflow/ui/src/queries/useDags.tsx index 43d2cab576969..759180cb16a4a 100644 --- a/airflow-core/src/airflow/ui/src/queries/useDags.tsx +++ b/airflow-core/src/airflow/ui/src/queries/useDags.tsx @@ -38,6 +38,7 @@ export const useDags = ({ tags, tagsMatchMode, teams, + timetableType, }: { advancedSearch?: boolean; dagDisplayNamePattern?: string; @@ -56,6 +57,7 @@ export const useDags = ({ tags?: Array; tagsMatchMode?: "all" | "any"; teams?: Array; + timetableType?: Array; }) => { const refetchInterval = useAutoRefresh({}); @@ -78,6 +80,7 @@ export const useDags = ({ tags, tagsMatchMode, teams, + timetableType, }, undefined, { diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dags.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dags.py index 837e7805c6994..4b86ef251c577 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dags.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dags.py @@ -135,6 +135,20 @@ def test_should_return_200(self, test_client, query_params, expected_ids, expect assert previous_run_after > dag_run["run_after"] previous_run_after = dag_run["run_after"] + @pytest.mark.usefixtures("configure_git_connection_for_dag_bundle") + @pytest.mark.parametrize( + ("query_params", "expected_ids"), + [ + ({"timetable_type": ["CronTriggerTimetable"], "exclude_stale": False}, [DAG3_ID]), + ({"timetable_type": ["NullTimetable"], "exclude_stale": False}, [DAG1_ID, DAG2_ID]), + ], + ) + def test_timetable_type_filter(self, test_client: TestClient, query_params, expected_ids): + response = test_client.get("/dags", params=query_params) + + assert response.status_code == 200 + assert [dag["dag_id"] for dag in response.json()["dags"]] == expected_ids + @pytest.mark.usefixtures("configure_git_connection_for_dag_bundle") @pytest.mark.parametrize("state", ["queued", "running", "failed", "success"]) def test_dag_run_state_matches_any_run_not_only_latest(self, test_client, session, state): From 73996b52e78e84ca265ea1aed79c29cf246ced6f Mon Sep 17 00:00:00 2001 From: Shivam Rastogi <6463385+shivaam@users.noreply.github.com> Date: Mon, 13 Jul 2026 02:53:44 -0700 Subject: [PATCH 2/4] UI: Keep timetable filtering consistent with Dag search Keeping these text inputs on one implementation prevents their debounce, clearing, and URL synchronization behavior from drifting. --- .../ui/src/components/SearchBar.test.tsx | 23 +++++++ .../airflow/ui/src/components/SearchBar.tsx | 35 ++++++++-- .../DagsFilters/TimetableTypeFilter.tsx | 65 ++++--------------- 3 files changed, 63 insertions(+), 60 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/components/SearchBar.test.tsx b/airflow-core/src/airflow/ui/src/components/SearchBar.test.tsx index dba492e5f8a74..6723ffb2a30dd 100644 --- a/airflow-core/src/airflow/ui/src/components/SearchBar.test.tsx +++ b/airflow-core/src/airflow/ui/src/components/SearchBar.test.tsx @@ -151,6 +151,29 @@ describe("Test SearchBar", () => { expect(screen.queryByTestId("advanced-search-toggle")).toBeNull(); }); + it("supports custom input presentation", () => { + render( + } + />, + { wrapper: Wrapper }, + ); + + expect(screen.getByTestId("timetable-type").getAttribute("aria-label")).toBe("Timetable type"); + expect(screen.getByTestId("timetable-icon").isConnected).toBe(true); + expect(screen.getByTestId("clear-timetable-type").getAttribute("aria-label")).toBe( + "Clear timetable type", + ); + }); + it("renders advanced toggle and reflects enabled state", () => { const onToggle = vi.fn(); diff --git a/airflow-core/src/airflow/ui/src/components/SearchBar.tsx b/airflow-core/src/airflow/ui/src/components/SearchBar.tsx index 57981808c6250..040fc5b774ccc 100644 --- a/airflow-core/src/airflow/ui/src/components/SearchBar.tsx +++ b/airflow-core/src/airflow/ui/src/components/SearchBar.tsx @@ -16,8 +16,16 @@ * specific language governing permissions and limitations * under the License. */ -import { CloseButton, HStack, Input, InputGroup, Kbd, type InputGroupProps } from "@chakra-ui/react"; -import { useEffect, useRef, useState, type ChangeEvent } from "react"; +import { + CloseButton, + HStack, + Input, + InputGroup, + Kbd, + type InputGroupProps, + type InputProps, +} from "@chakra-ui/react"; +import { useEffect, useRef, useState, type ChangeEvent, type ReactNode } from "react"; import { useTranslation } from "react-i18next"; import { FiSearch } from "react-icons/fi"; import { useDebouncedCallback } from "use-debounce"; @@ -33,18 +41,30 @@ type AdvancedSearchProps = Omit; type Props = { readonly advancedSearch?: AdvancedSearchProps; + readonly clearButtonAriaLabel?: string; + readonly clearButtonTestId?: string; readonly defaultValue: string; readonly hotkeyDisabled?: boolean; + readonly inputAriaLabel?: string; + readonly inputPaddingRight?: InputProps["paddingRight"]; + readonly inputTestId?: string; readonly onChange: (value: string) => void; readonly placeholder: string; + readonly startElement?: ReactNode; } & Omit; export const SearchBar = ({ advancedSearch, + clearButtonAriaLabel, + clearButtonTestId = "clear-search", defaultValue, hotkeyDisabled = false, + inputAriaLabel, + inputPaddingRight = 150, + inputTestId = "search-dags", onChange, placeholder, + startElement, ...props }: Props) => { const lastSentValue = useRef(defaultValue); @@ -91,8 +111,8 @@ export const SearchBar = ({ <> {Boolean(value) ? ( @@ -105,13 +125,14 @@ export const SearchBar = ({ )} } - startElement={} + startElement={startElement ?? } > diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TimetableTypeFilter.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TimetableTypeFilter.tsx index 06d325e4fc527..a7b3a93cb2be6 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TimetableTypeFilter.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TimetableTypeFilter.tsx @@ -16,13 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -import { CloseButton, Input, InputGroup } from "@chakra-ui/react"; -import { useEffect, useRef, useState, type ChangeEvent } from "react"; import { useTranslation } from "react-i18next"; import { FiClock } from "react-icons/fi"; -import { useDebouncedCallback } from "use-debounce"; -const debounceDelay = 200; +import { SearchBar } from "src/components/SearchBar"; type Props = { readonly onChange: (value: string) => void; @@ -30,60 +27,22 @@ type Props = { }; export const TimetableTypeFilter = ({ onChange, value: initialValue }: Props) => { - const [value, setValue] = useState(initialValue); - const lastSentValue = useRef(initialValue); const { t: translate } = useTranslation("dags"); - const handleFilterChange = useDebouncedCallback((newValue: string) => { - lastSentValue.current = newValue; - onChange(newValue); - }, debounceDelay); - - useEffect(() => { - if (initialValue !== lastSentValue.current) { - setValue(initialValue); - lastSentValue.current = initialValue; - } - }, [initialValue]); - - const onInputChange = (event: ChangeEvent) => { - const newValue = event.target.value; - - setValue(newValue); - handleFilterChange(newValue.trim()); - }; - - const clearFilter = () => { - handleFilterChange.cancel(); - lastSentValue.current = ""; - setValue(""); - onChange(""); - }; - return ( - - ) : undefined - } + onChange(newValue.trim())} + placeholder={translate("filters.timetableType")} startElement={} - > - - + /> ); }; From c5dacaf9521cf620e28da893ecd57e42586a8a5d Mon Sep 17 00:00:00 2001 From: Shivam Rastogi <6463385+shivaam@users.noreply.github.com> Date: Fri, 24 Jul 2026 21:14:52 -0700 Subject: [PATCH 3/4] UI: Add discoverable timetable type filtering Dags can use built-in or plugin timetables whose persisted type names are not discoverable from free text alone. Exposing readable values lets users narrow large Dag lists without knowing the exact class path. --- airflow-core/newsfragments/69728.feature.rst | 1 + .../airflow/api_fastapi/common/parameters.py | 4 + .../core_api/datamodels/ui/dags.py | 7 ++ .../core_api/openapi/_private_ui.yaml | 80 +++++++++++++++++++ .../api_fastapi/core_api/routes/ui/dags.py | 39 ++++++++- .../airflow/ui/openapi-gen/queries/common.ts | 13 ++- .../ui/openapi-gen/queries/ensureQueryData.ts | 21 ++++- .../ui/openapi-gen/queries/prefetch.ts | 21 ++++- .../airflow/ui/openapi-gen/queries/queries.ts | 21 ++++- .../ui/openapi-gen/queries/suspense.ts | 21 ++++- .../ui/openapi-gen/requests/schemas.gen.ts | 20 +++++ .../ui/openapi-gen/requests/services.gen.ts | 29 ++++++- .../ui/openapi-gen/requests/types.gen.ts | 35 ++++++++ .../ui/public/i18n/locales/en/dags.json | 4 +- .../ui/src/components/SearchBar.test.tsx | 23 ------ .../airflow/ui/src/components/SearchBar.tsx | 35 ++------ .../src/airflow/ui/src/mocks/handlers/dags.ts | 12 +++ .../DagsList/DagsFilters/DagsFilters.test.tsx | 48 ++++++++++- .../DagsList/DagsFilters/DagsFilters.tsx | 59 ++++++++++---- .../pages/DagsList/DagsFilters/TagFilter.tsx | 5 +- .../pages/DagsList/DagsFilters/TeamFilter.tsx | 5 +- .../DagsFilters/TimetableTypeFilter.tsx | 74 ++++++++++++----- .../ui/src/pages/DagsList/DagsList.tsx | 2 +- .../queries/useDagTimetableTypesInfinite.ts | 68 ++++++++++++++++ .../core_api/routes/ui/test_dags.py | 79 ++++++++++++++++++ 25 files changed, 614 insertions(+), 112 deletions(-) create mode 100644 airflow-core/newsfragments/69728.feature.rst create mode 100644 airflow-core/src/airflow/ui/src/queries/useDagTimetableTypesInfinite.ts diff --git a/airflow-core/newsfragments/69728.feature.rst b/airflow-core/newsfragments/69728.feature.rst new file mode 100644 index 0000000000000..01ab7439c18ba --- /dev/null +++ b/airflow-core/newsfragments/69728.feature.rst @@ -0,0 +1 @@ +Add timetable type filtering to the Dags list. diff --git a/airflow-core/src/airflow/api_fastapi/common/parameters.py b/airflow-core/src/airflow/api_fastapi/common/parameters.py index 1f940dcfd5a57..286fa75ec49db 100644 --- a/airflow-core/src/airflow/api_fastapi/common/parameters.py +++ b/airflow-core/src/airflow/api_fastapi/common/parameters.py @@ -1321,6 +1321,10 @@ def depends_float( _PrefixSearchParam, Depends(prefix_search_param_factory(DagModel.dag_display_name, "dag_display_name_prefix_pattern")), ] +QueryTimetableTypePrefixPatternSearch = Annotated[ + _PrefixSearchParam, + Depends(prefix_search_param_factory(DagModel.timetable_type, "timetable_type_prefix_pattern")), +] QueryBundleNameFilter = Annotated[ FilterParam[str | None], Depends(filter_param_factory(DagModel.bundle_name, str | None, filter_name="bundle_name")), diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/dags.py b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/dags.py index e49e89087daeb..6612906a6eb55 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/dags.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/dags.py @@ -42,6 +42,13 @@ class DAGWithLatestDagRunsCollectionResponse(BaseModel): dags: list[DAGWithLatestDagRunsResponse] +class DagTimetableTypeCollectionResponse(BaseModel): + """Timetable types used by Dags.""" + + timetable_types: list[str] + total_entries: int + + class DAGRunStateCountsResponse(BaseModel): """Per-Dag counts of DagRuns grouped by state.""" 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 26a5a8e499ea7..553e25520585d 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 @@ -580,6 +580,70 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' + /ui/dags/timetable_types: + get: + tags: + - DAG + summary: Get Dag Timetable Types + description: Get timetable types used by readable Dags. + operationId: get_dag_timetable_types_ui + security: + - OAuth2PasswordBearer: [] + - HTTPBearer: [] + parameters: + - name: limit + in: query + required: false + schema: + type: integer + minimum: 0 + default: 50 + title: Limit + - name: offset + in: query + required: false + schema: + type: integer + minimum: 0 + default: 0 + title: Offset + - name: timetable_type_prefix_pattern + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + description: "Prefix match \u2014 returns items whose value starts with\ + \ the given string (case-sensitive, index-friendly). Use the pipe `|`\ + \ operator for OR logic (e.g. `dag1|dag2`). Use `~` to match all. Wildcard\ + \ characters (`%`, `_`) are treated as literal characters. Trailing non-alphanumeric\ + \ characters in the prefix are stripped before matching so the range scan\ + \ stays index-compatible under locale-aware collations \u2014 e.g. `test_`\ + \ effectively matches items starting with `test`, and `s3://` matches\ + \ items starting with `s3`." + title: Timetable Type Prefix Pattern + description: "Prefix match \u2014 returns items whose value starts with the\ + \ given string (case-sensitive, index-friendly). Use the pipe `|` operator\ + \ for OR logic (e.g. `dag1|dag2`). Use `~` to match all. Wildcard characters\ + \ (`%`, `_`) are treated as literal characters. Trailing non-alphanumeric\ + \ characters in the prefix are stripped before matching so the range scan\ + \ stays index-compatible under locale-aware collations \u2014 e.g. `test_`\ + \ effectively matches items starting with `test`, and `s3://` matches items\ + \ starting with `s3`." + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/DagTimetableTypeCollectionResponse' + '422': + description: Validation Error + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' /ui/dags/{dag_id}/latest_run: get: tags: @@ -2768,6 +2832,22 @@ components: - dag_display_name title: DagTagResponse description: Dag Tag serializer for responses. + DagTimetableTypeCollectionResponse: + properties: + timetable_types: + items: + type: string + type: array + title: Timetable Types + total_entries: + type: integer + title: Total Entries + type: object + required: + - timetable_types + - total_entries + title: DagTimetableTypeCollectionResponse + description: Timetable types used by Dags. DagVersionResponse: properties: id: diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dags.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dags.py index b46c0ea0cab84..962196e51efdd 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dags.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dags.py @@ -17,10 +17,11 @@ from __future__ import annotations +from collections.abc import Sequence from typing import Annotated from fastapi import Depends, HTTPException, Query, status -from sqlalchemy import func, literal, select, union_all +from sqlalchemy import false, func, literal, select, union_all from sqlalchemy.orm import defaultload from airflow.api_fastapi.auth.managers.models.resource_details import DagAccessEntity @@ -52,6 +53,7 @@ QueryPendingActionsFilter, QueryTagsFilter, QueryTeamsFilter, + QueryTimetableTypePrefixPatternSearch, SortParam, filter_param_factory, ) @@ -61,6 +63,7 @@ from airflow.api_fastapi.core_api.datamodels.ui.dags import ( DAGRunStateCountsResponse, DAGsRunStateCountsCollectionResponse, + DagTimetableTypeCollectionResponse, DAGWithLatestDagRunsCollectionResponse, DAGWithLatestDagRunsResponse, ) @@ -280,6 +283,40 @@ def get_dags( ) +@dags_router.get( + "/timetable_types", + dependencies=[Depends(requires_access_dag(method="GET"))], + operation_id="get_dag_timetable_types_ui", +) +def get_dag_timetable_types( + limit: QueryLimit, + offset: QueryOffset, + timetable_type_prefix_pattern: QueryTimetableTypePrefixPatternSearch, + readable_dags_filter: ReadableDagsFilterDep, + session: SessionDep, +) -> DagTimetableTypeCollectionResponse: + """Get timetable types used by readable Dags.""" + query = ( + select(DagModel.timetable_type) + .where(DagModel.is_stale == false(), DagModel.timetable_type != "") + .group_by(DagModel.timetable_type) + ) + timetable_types_select, total_entries = paginated_select( + statement=query, + filters=[timetable_type_prefix_pattern, readable_dags_filter], + offset=offset, + limit=limit, + session=session, + ) + timetable_types: Sequence[str] = session.scalars( + timetable_types_select.order_by(DagModel.timetable_type) + ).all() + return DagTimetableTypeCollectionResponse( + timetable_types=list(timetable_types), + total_entries=total_entries, + ) + + @dags_router.get( "/{dag_id}/latest_run", responses=create_openapi_http_exception_doc([status.HTTP_404_NOT_FOUND]), diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts index 4f4852029734e..0fd23014323be 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts @@ -342,7 +342,7 @@ export const UseDagServiceGetDagTagsKeyFn = ({ limit, offset, orderBy, tagNamePa export type DagServiceGetDagsUiDefaultResponse = Awaited>; export type DagServiceGetDagsUiQueryResult = UseQueryResult; export const useDagServiceGetDagsUiKey = "DagServiceGetDagsUi"; -export const UseDagServiceGetDagsUiKeyFn = ({ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, teams }: { +export const UseDagServiceGetDagsUiKeyFn = ({ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, teams, timetableType }: { assetDependency?: string; bundleName?: string; bundleVersion?: string; @@ -367,7 +367,16 @@ export const UseDagServiceGetDagsUiKeyFn = ({ assetDependency, bundleName, bundl tags?: string[]; tagsMatchMode?: "any" | "all"; teams?: string[]; -} = {}, queryKey?: Array) => [useDagServiceGetDagsUiKey, ...(queryKey ?? [{ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, teams }])]; + timetableType?: string[]; +} = {}, queryKey?: Array) => [useDagServiceGetDagsUiKey, ...(queryKey ?? [{ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, teams, timetableType }])]; +export type DagServiceGetDagTimetableTypesUiDefaultResponse = Awaited>; +export type DagServiceGetDagTimetableTypesUiQueryResult = UseQueryResult; +export const useDagServiceGetDagTimetableTypesUiKey = "DagServiceGetDagTimetableTypesUi"; +export const UseDagServiceGetDagTimetableTypesUiKeyFn = ({ limit, offset, timetableTypePrefixPattern }: { + limit?: number; + offset?: number; + timetableTypePrefixPattern?: string; +} = {}, queryKey?: Array) => [useDagServiceGetDagTimetableTypesUiKey, ...(queryKey ?? [{ limit, offset, timetableTypePrefixPattern }])]; export type DagServiceGetLatestRunInfoDefaultResponse = Awaited>; export type DagServiceGetLatestRunInfoQueryResult = UseQueryResult; export const useDagServiceGetLatestRunInfoKey = "DagServiceGetLatestRunInfo"; diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts index 08ecd2f13f369..d02b1ae25bafe 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts @@ -702,11 +702,12 @@ export const ensureUseDagServiceGetDagTagsData = (queryClient: QueryClient, { li * @param data.isFavorite * @param data.hasAssetSchedule Filter Dags with asset-based scheduling * @param data.assetDependency Filter Dags by asset dependency (name or URI) +* @param data.timetableType * @param data.hasPendingActions * @returns DAGWithLatestDagRunsCollectionResponse Successful Response * @throws ApiError */ -export const ensureUseDagServiceGetDagsUiData = (queryClient: QueryClient, { assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, teams }: { +export const ensureUseDagServiceGetDagsUiData = (queryClient: QueryClient, { assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, teams, timetableType }: { assetDependency?: string; bundleName?: string; bundleVersion?: string; @@ -731,7 +732,23 @@ export const ensureUseDagServiceGetDagsUiData = (queryClient: QueryClient, { ass tags?: string[]; tagsMatchMode?: "any" | "all"; teams?: string[]; -} = {}) => queryClient.ensureQueryData({ queryKey: Common.UseDagServiceGetDagsUiKeyFn({ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, teams }), queryFn: () => DagService.getDagsUi({ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, teams }) }); + timetableType?: string[]; +} = {}) => queryClient.ensureQueryData({ queryKey: Common.UseDagServiceGetDagsUiKeyFn({ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, teams, timetableType }), queryFn: () => DagService.getDagsUi({ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, teams, timetableType }) }); +/** +* Get Dag Timetable Types +* Get timetable types used by readable Dags. +* @param data The data for the request. +* @param data.limit +* @param data.offset +* @param data.timetableTypePrefixPattern Prefix match — returns items whose value starts with the given string (case-sensitive, index-friendly). Use the pipe `|` operator for OR logic (e.g. `dag1|dag2`). Use `~` to match all. Wildcard characters (`%`, `_`) are treated as literal characters. Trailing non-alphanumeric characters in the prefix are stripped before matching so the range scan stays index-compatible under locale-aware collations — e.g. `test_` effectively matches items starting with `test`, and `s3://` matches items starting with `s3`. +* @returns DagTimetableTypeCollectionResponse Successful Response +* @throws ApiError +*/ +export const ensureUseDagServiceGetDagTimetableTypesUiData = (queryClient: QueryClient, { limit, offset, timetableTypePrefixPattern }: { + limit?: number; + offset?: number; + timetableTypePrefixPattern?: string; +} = {}) => queryClient.ensureQueryData({ queryKey: Common.UseDagServiceGetDagTimetableTypesUiKeyFn({ limit, offset, timetableTypePrefixPattern }), queryFn: () => DagService.getDagTimetableTypesUi({ limit, offset, timetableTypePrefixPattern }) }); /** * Get Latest Run Info * Get latest run. diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts index 0b0d7b46eeb94..988e98b7ba2dc 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts @@ -702,11 +702,12 @@ export const prefetchUseDagServiceGetDagTags = (queryClient: QueryClient, { limi * @param data.isFavorite * @param data.hasAssetSchedule Filter Dags with asset-based scheduling * @param data.assetDependency Filter Dags by asset dependency (name or URI) +* @param data.timetableType * @param data.hasPendingActions * @returns DAGWithLatestDagRunsCollectionResponse Successful Response * @throws ApiError */ -export const prefetchUseDagServiceGetDagsUi = (queryClient: QueryClient, { assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, teams }: { +export const prefetchUseDagServiceGetDagsUi = (queryClient: QueryClient, { assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, teams, timetableType }: { assetDependency?: string; bundleName?: string; bundleVersion?: string; @@ -731,7 +732,23 @@ export const prefetchUseDagServiceGetDagsUi = (queryClient: QueryClient, { asset tags?: string[]; tagsMatchMode?: "any" | "all"; teams?: string[]; -} = {}) => queryClient.prefetchQuery({ queryKey: Common.UseDagServiceGetDagsUiKeyFn({ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, teams }), queryFn: () => DagService.getDagsUi({ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, teams }) }); + timetableType?: string[]; +} = {}) => queryClient.prefetchQuery({ queryKey: Common.UseDagServiceGetDagsUiKeyFn({ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, teams, timetableType }), queryFn: () => DagService.getDagsUi({ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, teams, timetableType }) }); +/** +* Get Dag Timetable Types +* Get timetable types used by readable Dags. +* @param data The data for the request. +* @param data.limit +* @param data.offset +* @param data.timetableTypePrefixPattern Prefix match — returns items whose value starts with the given string (case-sensitive, index-friendly). Use the pipe `|` operator for OR logic (e.g. `dag1|dag2`). Use `~` to match all. Wildcard characters (`%`, `_`) are treated as literal characters. Trailing non-alphanumeric characters in the prefix are stripped before matching so the range scan stays index-compatible under locale-aware collations — e.g. `test_` effectively matches items starting with `test`, and `s3://` matches items starting with `s3`. +* @returns DagTimetableTypeCollectionResponse Successful Response +* @throws ApiError +*/ +export const prefetchUseDagServiceGetDagTimetableTypesUi = (queryClient: QueryClient, { limit, offset, timetableTypePrefixPattern }: { + limit?: number; + offset?: number; + timetableTypePrefixPattern?: string; +} = {}) => queryClient.prefetchQuery({ queryKey: Common.UseDagServiceGetDagTimetableTypesUiKeyFn({ limit, offset, timetableTypePrefixPattern }), queryFn: () => DagService.getDagTimetableTypesUi({ limit, offset, timetableTypePrefixPattern }) }); /** * Get Latest Run Info * Get latest run. diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts index 59d5c2dce19f1..98f368ce9cadf 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts @@ -702,11 +702,12 @@ export const useDagServiceGetDagTags = = unknown[]>({ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, teams }: { +export const useDagServiceGetDagsUi = = unknown[]>({ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, teams, timetableType }: { assetDependency?: string; bundleName?: string; bundleVersion?: string; @@ -731,7 +732,23 @@ export const useDagServiceGetDagsUi = , "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseDagServiceGetDagsUiKeyFn({ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, teams }, queryKey), queryFn: () => DagService.getDagsUi({ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, teams }) as TData, ...options }); + timetableType?: string[]; +} = {}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseDagServiceGetDagsUiKeyFn({ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, teams, timetableType }, queryKey), queryFn: () => DagService.getDagsUi({ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, teams, timetableType }) as TData, ...options }); +/** +* Get Dag Timetable Types +* Get timetable types used by readable Dags. +* @param data The data for the request. +* @param data.limit +* @param data.offset +* @param data.timetableTypePrefixPattern Prefix match — returns items whose value starts with the given string (case-sensitive, index-friendly). Use the pipe `|` operator for OR logic (e.g. `dag1|dag2`). Use `~` to match all. Wildcard characters (`%`, `_`) are treated as literal characters. Trailing non-alphanumeric characters in the prefix are stripped before matching so the range scan stays index-compatible under locale-aware collations — e.g. `test_` effectively matches items starting with `test`, and `s3://` matches items starting with `s3`. +* @returns DagTimetableTypeCollectionResponse Successful Response +* @throws ApiError +*/ +export const useDagServiceGetDagTimetableTypesUi = = unknown[]>({ limit, offset, timetableTypePrefixPattern }: { + limit?: number; + offset?: number; + timetableTypePrefixPattern?: string; +} = {}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseDagServiceGetDagTimetableTypesUiKeyFn({ limit, offset, timetableTypePrefixPattern }, queryKey), queryFn: () => DagService.getDagTimetableTypesUi({ limit, offset, timetableTypePrefixPattern }) as TData, ...options }); /** * Get Latest Run Info * Get latest run. diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts index 26965f1a28612..172bdf927e13e 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts @@ -702,11 +702,12 @@ export const useDagServiceGetDagTagsSuspense = = unknown[]>({ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, teams }: { +export const useDagServiceGetDagsUiSuspense = = unknown[]>({ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, teams, timetableType }: { assetDependency?: string; bundleName?: string; bundleVersion?: string; @@ -731,7 +732,23 @@ export const useDagServiceGetDagsUiSuspense = , "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseDagServiceGetDagsUiKeyFn({ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, teams }, queryKey), queryFn: () => DagService.getDagsUi({ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, teams }) as TData, ...options }); + timetableType?: string[]; +} = {}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseDagServiceGetDagsUiKeyFn({ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, teams, timetableType }, queryKey), queryFn: () => DagService.getDagsUi({ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, teams, timetableType }) as TData, ...options }); +/** +* Get Dag Timetable Types +* Get timetable types used by readable Dags. +* @param data The data for the request. +* @param data.limit +* @param data.offset +* @param data.timetableTypePrefixPattern Prefix match — returns items whose value starts with the given string (case-sensitive, index-friendly). Use the pipe `|` operator for OR logic (e.g. `dag1|dag2`). Use `~` to match all. Wildcard characters (`%`, `_`) are treated as literal characters. Trailing non-alphanumeric characters in the prefix are stripped before matching so the range scan stays index-compatible under locale-aware collations — e.g. `test_` effectively matches items starting with `test`, and `s3://` matches items starting with `s3`. +* @returns DagTimetableTypeCollectionResponse Successful Response +* @throws ApiError +*/ +export const useDagServiceGetDagTimetableTypesUiSuspense = = unknown[]>({ limit, offset, timetableTypePrefixPattern }: { + limit?: number; + offset?: number; + timetableTypePrefixPattern?: string; +} = {}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseDagServiceGetDagTimetableTypesUiKeyFn({ limit, offset, timetableTypePrefixPattern }, queryKey), queryFn: () => DagService.getDagTimetableTypesUi({ limit, offset, timetableTypePrefixPattern }) as TData, ...options }); /** * Get Latest Run Info * Get latest run. 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 2e4c24e117063..b5009ba3bad1a 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 @@ -9522,6 +9522,26 @@ export const $DagRunStatsResponse = { description: 'DAG Run statistics serializer for responses.' } as const; +export const $DagTimetableTypeCollectionResponse = { + properties: { + timetable_types: { + items: { + type: 'string' + }, + type: 'array', + title: 'Timetable Types' + }, + total_entries: { + type: 'integer', + title: 'Total Entries' + } + }, + type: 'object', + required: ['timetable_types', 'total_entries'], + title: 'DagTimetableTypeCollectionResponse', + description: 'Timetable types used by Dags.' +} as const; + export const $DashboardDagStatsResponse = { properties: { active_dag_count: { diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts index 73b0a4c540746..49d06a820dc67 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts @@ -3,7 +3,7 @@ import type { CancelablePromise } from './core/CancelablePromise'; import { OpenAPI } from './core/OpenAPI'; import { request as __request } from './core/request'; -import type { GetAssetsData, GetAssetsResponse, GetAssetAliasesData, GetAssetAliasesResponse, GetAssetAliasData, GetAssetAliasResponse, GetAssetEventsData, GetAssetEventsResponse, CreateAssetEventData, CreateAssetEventResponse, MaterializeAssetData, MaterializeAssetResponse, GetAssetQueuedEventsData, GetAssetQueuedEventsResponse, DeleteAssetQueuedEventsData, DeleteAssetQueuedEventsResponse, GetAssetData, GetAssetResponse, GetDagAssetQueuedEventsData, GetDagAssetQueuedEventsResponse, DeleteDagAssetQueuedEventsData, DeleteDagAssetQueuedEventsResponse, GetDagAssetQueuedEventData, GetDagAssetQueuedEventResponse, DeleteDagAssetQueuedEventData, DeleteDagAssetQueuedEventResponse, NextRunAssetsData, NextRunAssetsResponse2, ListBackfillsData, ListBackfillsResponse, CreateBackfillData, CreateBackfillResponse, GetBackfillData, GetBackfillResponse, ListBackfillDagRunsData, ListBackfillDagRunsResponse, PauseBackfillData, PauseBackfillResponse, UnpauseBackfillData, UnpauseBackfillResponse, CancelBackfillData, CancelBackfillResponse, CreateBackfillDryRunData, CreateBackfillDryRunResponse, ListBackfillsUiData, ListBackfillsUiResponse, DeleteConnectionData, DeleteConnectionResponse, GetConnectionData, GetConnectionResponse, PatchConnectionData, PatchConnectionResponse, GetConnectionTestData, GetConnectionTestResponse, EnqueueConnectionTestData, EnqueueConnectionTestResponse, GetConnectionsData, GetConnectionsResponse, PostConnectionData, PostConnectionResponse, BulkConnectionsData, BulkConnectionsResponse, TestConnectionData, TestConnectionResponse, CreateDefaultConnectionsResponse, HookMetaDataResponse, GetDagRunData, GetDagRunResponse, DeleteDagRunData, DeleteDagRunResponse, PatchDagRunData, PatchDagRunResponse, BulkDagRunsData, BulkDagRunsResponse, GetDagRunsData, GetDagRunsResponse, TriggerDagRunData, TriggerDagRunResponse, GetUpstreamAssetEventsData, GetUpstreamAssetEventsResponse, ClearDagRunData, ClearDagRunResponse, WaitDagRunUntilFinishedData, WaitDagRunUntilFinishedResponse, GetListDagRunsBatchData, GetListDagRunsBatchResponse, ClearDagRunsData, ClearDagRunsResponse, ClearDagRunPartitionsData, ClearDagRunPartitionsResponse, GetDagRunStatsData, GetDagRunStatsResponse, GetDagSourceData, GetDagSourceResponse, GetDagStatsData, GetDagStatsResponse, GetConfigData, GetConfigResponse, GetConfigValueData, GetConfigValueResponse, GetConfigsResponse, ListDagWarningsData, ListDagWarningsResponse, GetDagsData, GetDagsResponse, PatchDagsData, PatchDagsResponse, GetDagData, GetDagResponse, PatchDagData, PatchDagResponse, DeleteDagData, DeleteDagResponse, GetDagDetailsData, GetDagDetailsResponse, FavoriteDagData, FavoriteDagResponse, UnfavoriteDagData, UnfavoriteDagResponse, GetDagTagsData, GetDagTagsResponse, GetDagsUiData, GetDagsUiResponse, GetLatestRunInfoData, GetLatestRunInfoResponse, GetDagRunStateCountsUiData, GetDagRunStateCountsUiResponse, GetEventLogData, GetEventLogResponse, GetEventLogsData, GetEventLogsResponse, GetExtraLinksData, GetExtraLinksResponse, GetTaskInstanceData, GetTaskInstanceResponse, PatchTaskInstanceData, PatchTaskInstanceResponse, DeleteTaskInstanceData, DeleteTaskInstanceResponse, GetMappedTaskInstancesData, GetMappedTaskInstancesResponse, GetTaskInstanceDependenciesByMapIndexData, GetTaskInstanceDependenciesByMapIndexResponse, GetTaskInstanceDependenciesData, GetTaskInstanceDependenciesResponse, GetTaskInstanceTriesData, GetTaskInstanceTriesResponse, GetMappedTaskInstanceTriesData, GetMappedTaskInstanceTriesResponse, GetMappedTaskInstanceData, GetMappedTaskInstanceResponse, PatchTaskInstanceByMapIndexData, PatchTaskInstanceByMapIndexResponse, GetTaskInstancesData, GetTaskInstancesResponse, BulkTaskInstancesData, BulkTaskInstancesResponse, GetTaskInstancesBatchData, GetTaskInstancesBatchResponse, GetTaskInstanceTryDetailsData, GetTaskInstanceTryDetailsResponse, GetMappedTaskInstanceTryDetailsData, GetMappedTaskInstanceTryDetailsResponse, PostClearTaskInstancesData, PostClearTaskInstancesResponse, PatchTaskGroupInstancesData, PatchTaskGroupInstancesResponse, PatchTaskGroupInstancesDryRunData, PatchTaskGroupInstancesDryRunResponse, PatchTaskInstanceDryRunByMapIndexData, PatchTaskInstanceDryRunByMapIndexResponse, PatchTaskInstanceDryRunData, PatchTaskInstanceDryRunResponse, GetLogData, GetLogResponse, GetExternalLogUrlData, GetExternalLogUrlResponse, UpdateHitlDetailData, UpdateHitlDetailResponse, GetHitlDetailData, GetHitlDetailResponse, GetHitlDetailTryDetailData, GetHitlDetailTryDetailResponse, GetHitlDetailsData, GetHitlDetailsResponse, GetImportErrorData, GetImportErrorResponse, GetImportErrorsData, GetImportErrorsResponse, GetJobsData, GetJobsResponse, GetPluginsData, GetPluginsResponse, ImportErrorsResponse, DeletePoolData, DeletePoolResponse, GetPoolData, GetPoolResponse, PatchPoolData, PatchPoolResponse, GetPoolsData, GetPoolsResponse, PostPoolData, PostPoolResponse, BulkPoolsData, BulkPoolsResponse, GetProvidersData, GetProvidersResponse, ListAssetStateStoreData, ListAssetStateStoreResponse, ClearAssetStateStoreData, ClearAssetStateStoreResponse, GetAssetStateStoreData, GetAssetStateStoreResponse, SetAssetStateStoreData, SetAssetStateStoreResponse, DeleteAssetStateStoreData, DeleteAssetStateStoreResponse, ListTaskStateStoreData, ListTaskStateStoreResponse, ClearTaskStateStoreData, ClearTaskStateStoreResponse, GetTaskStateStoreData, GetTaskStateStoreResponse, SetTaskStateStoreData, SetTaskStateStoreResponse, PatchTaskStateStoreData, PatchTaskStateStoreResponse, DeleteTaskStateStoreData, DeleteTaskStateStoreResponse, GetXcomEntryData, GetXcomEntryResponse, UpdateXcomEntryData, UpdateXcomEntryResponse, DeleteXcomEntryData, DeleteXcomEntryResponse, GetXcomEntriesData, GetXcomEntriesResponse, CreateXcomEntryData, CreateXcomEntryResponse, GetTasksData, GetTasksResponse, GetTaskData, GetTaskResponse, DeleteVariableData, DeleteVariableResponse, GetVariableData, GetVariableResponse, PatchVariableData, PatchVariableResponse, GetVariablesData, GetVariablesResponse, PostVariableData, PostVariableResponse, BulkVariablesData, BulkVariablesResponse, ReparseDagFileData, ReparseDagFileResponse, GetDagVersionData, GetDagVersionResponse, GetDagVersionsData, GetDagVersionsResponse, GetHealthResponse, GetVersionResponse, LoginData, LoginResponse, LogoutResponse, GetAuthMenusResponse, GetCurrentUserInfoResponse, GenerateTokenData, GenerateTokenResponse2, GetPartitionedDagRunsData, GetPartitionedDagRunsResponse, GetPendingPartitionedDagRunData, GetPendingPartitionedDagRunResponse, GetDependenciesData, GetDependenciesResponse, HistoricalMetricsData, HistoricalMetricsResponse, DagStatsResponse2, GetDeadlinesData, GetDeadlinesResponse, GetDagDeadlineAlertsData, GetDagDeadlineAlertsResponse, StructureDataData, StructureDataResponse2, GetDagStructureData, GetDagStructureResponse, GetGridRunsData, GetGridRunsResponse, GetGridTiSummariesStreamData, GetGridTiSummariesStreamResponse, GetGanttDataData, GetGanttDataResponse, GetCalendarData, GetCalendarResponse, GetCalendarDeadlinesData, GetCalendarDeadlinesResponse, ListTeamsData, ListTeamsResponse } from './types.gen'; +import type { GetAssetsData, GetAssetsResponse, GetAssetAliasesData, GetAssetAliasesResponse, GetAssetAliasData, GetAssetAliasResponse, GetAssetEventsData, GetAssetEventsResponse, CreateAssetEventData, CreateAssetEventResponse, MaterializeAssetData, MaterializeAssetResponse, GetAssetQueuedEventsData, GetAssetQueuedEventsResponse, DeleteAssetQueuedEventsData, DeleteAssetQueuedEventsResponse, GetAssetData, GetAssetResponse, GetDagAssetQueuedEventsData, GetDagAssetQueuedEventsResponse, DeleteDagAssetQueuedEventsData, DeleteDagAssetQueuedEventsResponse, GetDagAssetQueuedEventData, GetDagAssetQueuedEventResponse, DeleteDagAssetQueuedEventData, DeleteDagAssetQueuedEventResponse, NextRunAssetsData, NextRunAssetsResponse2, ListBackfillsData, ListBackfillsResponse, CreateBackfillData, CreateBackfillResponse, GetBackfillData, GetBackfillResponse, ListBackfillDagRunsData, ListBackfillDagRunsResponse, PauseBackfillData, PauseBackfillResponse, UnpauseBackfillData, UnpauseBackfillResponse, CancelBackfillData, CancelBackfillResponse, CreateBackfillDryRunData, CreateBackfillDryRunResponse, ListBackfillsUiData, ListBackfillsUiResponse, DeleteConnectionData, DeleteConnectionResponse, GetConnectionData, GetConnectionResponse, PatchConnectionData, PatchConnectionResponse, GetConnectionTestData, GetConnectionTestResponse, EnqueueConnectionTestData, EnqueueConnectionTestResponse, GetConnectionsData, GetConnectionsResponse, PostConnectionData, PostConnectionResponse, BulkConnectionsData, BulkConnectionsResponse, TestConnectionData, TestConnectionResponse, CreateDefaultConnectionsResponse, HookMetaDataResponse, GetDagRunData, GetDagRunResponse, DeleteDagRunData, DeleteDagRunResponse, PatchDagRunData, PatchDagRunResponse, BulkDagRunsData, BulkDagRunsResponse, GetDagRunsData, GetDagRunsResponse, TriggerDagRunData, TriggerDagRunResponse, GetUpstreamAssetEventsData, GetUpstreamAssetEventsResponse, ClearDagRunData, ClearDagRunResponse, WaitDagRunUntilFinishedData, WaitDagRunUntilFinishedResponse, GetListDagRunsBatchData, GetListDagRunsBatchResponse, ClearDagRunsData, ClearDagRunsResponse, ClearDagRunPartitionsData, ClearDagRunPartitionsResponse, GetDagRunStatsData, GetDagRunStatsResponse, GetDagSourceData, GetDagSourceResponse, GetDagStatsData, GetDagStatsResponse, GetConfigData, GetConfigResponse, GetConfigValueData, GetConfigValueResponse, GetConfigsResponse, ListDagWarningsData, ListDagWarningsResponse, GetDagsData, GetDagsResponse, PatchDagsData, PatchDagsResponse, GetDagData, GetDagResponse, PatchDagData, PatchDagResponse, DeleteDagData, DeleteDagResponse, GetDagDetailsData, GetDagDetailsResponse, FavoriteDagData, FavoriteDagResponse, UnfavoriteDagData, UnfavoriteDagResponse, GetDagTagsData, GetDagTagsResponse, GetDagsUiData, GetDagsUiResponse, GetDagTimetableTypesUiData, GetDagTimetableTypesUiResponse, GetLatestRunInfoData, GetLatestRunInfoResponse, GetDagRunStateCountsUiData, GetDagRunStateCountsUiResponse, GetEventLogData, GetEventLogResponse, GetEventLogsData, GetEventLogsResponse, GetExtraLinksData, GetExtraLinksResponse, GetTaskInstanceData, GetTaskInstanceResponse, PatchTaskInstanceData, PatchTaskInstanceResponse, DeleteTaskInstanceData, DeleteTaskInstanceResponse, GetMappedTaskInstancesData, GetMappedTaskInstancesResponse, GetTaskInstanceDependenciesByMapIndexData, GetTaskInstanceDependenciesByMapIndexResponse, GetTaskInstanceDependenciesData, GetTaskInstanceDependenciesResponse, GetTaskInstanceTriesData, GetTaskInstanceTriesResponse, GetMappedTaskInstanceTriesData, GetMappedTaskInstanceTriesResponse, GetMappedTaskInstanceData, GetMappedTaskInstanceResponse, PatchTaskInstanceByMapIndexData, PatchTaskInstanceByMapIndexResponse, GetTaskInstancesData, GetTaskInstancesResponse, BulkTaskInstancesData, BulkTaskInstancesResponse, GetTaskInstancesBatchData, GetTaskInstancesBatchResponse, GetTaskInstanceTryDetailsData, GetTaskInstanceTryDetailsResponse, GetMappedTaskInstanceTryDetailsData, GetMappedTaskInstanceTryDetailsResponse, PostClearTaskInstancesData, PostClearTaskInstancesResponse, PatchTaskGroupInstancesData, PatchTaskGroupInstancesResponse, PatchTaskGroupInstancesDryRunData, PatchTaskGroupInstancesDryRunResponse, PatchTaskInstanceDryRunByMapIndexData, PatchTaskInstanceDryRunByMapIndexResponse, PatchTaskInstanceDryRunData, PatchTaskInstanceDryRunResponse, GetLogData, GetLogResponse, GetExternalLogUrlData, GetExternalLogUrlResponse, UpdateHitlDetailData, UpdateHitlDetailResponse, GetHitlDetailData, GetHitlDetailResponse, GetHitlDetailTryDetailData, GetHitlDetailTryDetailResponse, GetHitlDetailsData, GetHitlDetailsResponse, GetImportErrorData, GetImportErrorResponse, GetImportErrorsData, GetImportErrorsResponse, GetJobsData, GetJobsResponse, GetPluginsData, GetPluginsResponse, ImportErrorsResponse, DeletePoolData, DeletePoolResponse, GetPoolData, GetPoolResponse, PatchPoolData, PatchPoolResponse, GetPoolsData, GetPoolsResponse, PostPoolData, PostPoolResponse, BulkPoolsData, BulkPoolsResponse, GetProvidersData, GetProvidersResponse, ListAssetStateStoreData, ListAssetStateStoreResponse, ClearAssetStateStoreData, ClearAssetStateStoreResponse, GetAssetStateStoreData, GetAssetStateStoreResponse, SetAssetStateStoreData, SetAssetStateStoreResponse, DeleteAssetStateStoreData, DeleteAssetStateStoreResponse, ListTaskStateStoreData, ListTaskStateStoreResponse, ClearTaskStateStoreData, ClearTaskStateStoreResponse, GetTaskStateStoreData, GetTaskStateStoreResponse, SetTaskStateStoreData, SetTaskStateStoreResponse, PatchTaskStateStoreData, PatchTaskStateStoreResponse, DeleteTaskStateStoreData, DeleteTaskStateStoreResponse, GetXcomEntryData, GetXcomEntryResponse, UpdateXcomEntryData, UpdateXcomEntryResponse, DeleteXcomEntryData, DeleteXcomEntryResponse, GetXcomEntriesData, GetXcomEntriesResponse, CreateXcomEntryData, CreateXcomEntryResponse, GetTasksData, GetTasksResponse, GetTaskData, GetTaskResponse, DeleteVariableData, DeleteVariableResponse, GetVariableData, GetVariableResponse, PatchVariableData, PatchVariableResponse, GetVariablesData, GetVariablesResponse, PostVariableData, PostVariableResponse, BulkVariablesData, BulkVariablesResponse, ReparseDagFileData, ReparseDagFileResponse, GetDagVersionData, GetDagVersionResponse, GetDagVersionsData, GetDagVersionsResponse, GetHealthResponse, GetVersionResponse, LoginData, LoginResponse, LogoutResponse, GetAuthMenusResponse, GetCurrentUserInfoResponse, GenerateTokenData, GenerateTokenResponse2, GetPartitionedDagRunsData, GetPartitionedDagRunsResponse, GetPendingPartitionedDagRunData, GetPendingPartitionedDagRunResponse, GetDependenciesData, GetDependenciesResponse, HistoricalMetricsData, HistoricalMetricsResponse, DagStatsResponse2, GetDeadlinesData, GetDeadlinesResponse, GetDagDeadlineAlertsData, GetDagDeadlineAlertsResponse, StructureDataData, StructureDataResponse2, GetDagStructureData, GetDagStructureResponse, GetGridRunsData, GetGridRunsResponse, GetGridTiSummariesStreamData, GetGridTiSummariesStreamResponse, GetGanttDataData, GetGanttDataResponse, GetCalendarData, GetCalendarResponse, GetCalendarDeadlinesData, GetCalendarDeadlinesResponse, ListTeamsData, ListTeamsResponse } from './types.gen'; export class AssetService { /** @@ -2019,6 +2019,31 @@ export class DagService { }); } + /** + * Get Dag Timetable Types + * Get timetable types used by readable Dags. + * @param data The data for the request. + * @param data.limit + * @param data.offset + * @param data.timetableTypePrefixPattern Prefix match — returns items whose value starts with the given string (case-sensitive, index-friendly). Use the pipe `|` operator for OR logic (e.g. `dag1|dag2`). Use `~` to match all. Wildcard characters (`%`, `_`) are treated as literal characters. Trailing non-alphanumeric characters in the prefix are stripped before matching so the range scan stays index-compatible under locale-aware collations — e.g. `test_` effectively matches items starting with `test`, and `s3://` matches items starting with `s3`. + * @returns DagTimetableTypeCollectionResponse Successful Response + * @throws ApiError + */ + public static getDagTimetableTypesUi(data: GetDagTimetableTypesUiData = {}): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/ui/dags/timetable_types', + query: { + limit: data.limit, + offset: data.offset, + timetable_type_prefix_pattern: data.timetableTypePrefixPattern + }, + errors: { + 422: 'Validation Error' + } + }); + } + /** * Get Latest Run Info * Get latest run. @@ -5257,4 +5282,4 @@ export class TeamsService { }); } -} \ No newline at end of file +} 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 0d36910280ac8..5387c4f0ca8fd 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 @@ -2406,6 +2406,14 @@ export type DagRunStatsResponse = { duration: DurationStats | null; }; +/** + * Timetable types used by Dags. + */ +export type DagTimetableTypeCollectionResponse = { + timetable_types: Array<(string)>; + total_entries: number; +}; + /** * Dashboard DAG Stats serializer for responses. */ @@ -3579,10 +3587,22 @@ export type GetDagsUiData = { tags?: Array<(string)>; tagsMatchMode?: 'any' | 'all' | null; teams?: Array<(string)>; + timetableType?: Array<(string)>; }; export type GetDagsUiResponse = DAGWithLatestDagRunsCollectionResponse; +export type GetDagTimetableTypesUiData = { + limit?: number; + offset?: number; + /** + * Prefix match — returns items whose value starts with the given string (case-sensitive, index-friendly). Use the pipe `|` operator for OR logic (e.g. `dag1|dag2`). Use `~` to match all. Wildcard characters (`%`, `_`) are treated as literal characters. Trailing non-alphanumeric characters in the prefix are stripped before matching so the range scan stays index-compatible under locale-aware collations — e.g. `test_` effectively matches items starting with `test`, and `s3://` matches items starting with `s3`. + */ + timetableTypePrefixPattern?: string | null; +}; + +export type GetDagTimetableTypesUiResponse = DagTimetableTypeCollectionResponse; + export type GetLatestRunInfoData = { dagId: string; }; @@ -6482,6 +6502,21 @@ export type $OpenApiTs = { }; }; }; + '/ui/dags/timetable_types': { + get: { + req: GetDagTimetableTypesUiData; + res: { + /** + * Successful Response + */ + 200: DagTimetableTypeCollectionResponse; + /** + * Validation Error + */ + 422: HTTPValidationError; + }; + }; + }; '/ui/dags/{dag_id}/latest_run': { get: { req: GetLatestRunInfoData; diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/dags.json b/airflow-core/src/airflow/ui/public/i18n/locales/en/dags.json index 7cd6071762a6c..753f7fe53fd85 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/en/dags.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/dags.json @@ -17,14 +17,14 @@ "unfavorite": "Unfavorite" }, "lastRunState": "Last run", + "noTimetableTypesFound": "No timetable types found", "paused": { "active": "Active", "all": "All", "paused": "Paused" }, "runIdPatternFilter": "Search Dag Runs", - "timetableType": "Timetable type", - "timetableTypeClear": "Clear timetable type filter" + "timetableType": "Timetable type" }, "ownerLink": "Owner link for {{owner}}", "runAndTaskActions": { diff --git a/airflow-core/src/airflow/ui/src/components/SearchBar.test.tsx b/airflow-core/src/airflow/ui/src/components/SearchBar.test.tsx index 6723ffb2a30dd..dba492e5f8a74 100644 --- a/airflow-core/src/airflow/ui/src/components/SearchBar.test.tsx +++ b/airflow-core/src/airflow/ui/src/components/SearchBar.test.tsx @@ -151,29 +151,6 @@ describe("Test SearchBar", () => { expect(screen.queryByTestId("advanced-search-toggle")).toBeNull(); }); - it("supports custom input presentation", () => { - render( - } - />, - { wrapper: Wrapper }, - ); - - expect(screen.getByTestId("timetable-type").getAttribute("aria-label")).toBe("Timetable type"); - expect(screen.getByTestId("timetable-icon").isConnected).toBe(true); - expect(screen.getByTestId("clear-timetable-type").getAttribute("aria-label")).toBe( - "Clear timetable type", - ); - }); - it("renders advanced toggle and reflects enabled state", () => { const onToggle = vi.fn(); diff --git a/airflow-core/src/airflow/ui/src/components/SearchBar.tsx b/airflow-core/src/airflow/ui/src/components/SearchBar.tsx index 040fc5b774ccc..57981808c6250 100644 --- a/airflow-core/src/airflow/ui/src/components/SearchBar.tsx +++ b/airflow-core/src/airflow/ui/src/components/SearchBar.tsx @@ -16,16 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import { - CloseButton, - HStack, - Input, - InputGroup, - Kbd, - type InputGroupProps, - type InputProps, -} from "@chakra-ui/react"; -import { useEffect, useRef, useState, type ChangeEvent, type ReactNode } from "react"; +import { CloseButton, HStack, Input, InputGroup, Kbd, type InputGroupProps } from "@chakra-ui/react"; +import { useEffect, useRef, useState, type ChangeEvent } from "react"; import { useTranslation } from "react-i18next"; import { FiSearch } from "react-icons/fi"; import { useDebouncedCallback } from "use-debounce"; @@ -41,30 +33,18 @@ type AdvancedSearchProps = Omit; type Props = { readonly advancedSearch?: AdvancedSearchProps; - readonly clearButtonAriaLabel?: string; - readonly clearButtonTestId?: string; readonly defaultValue: string; readonly hotkeyDisabled?: boolean; - readonly inputAriaLabel?: string; - readonly inputPaddingRight?: InputProps["paddingRight"]; - readonly inputTestId?: string; readonly onChange: (value: string) => void; readonly placeholder: string; - readonly startElement?: ReactNode; } & Omit; export const SearchBar = ({ advancedSearch, - clearButtonAriaLabel, - clearButtonTestId = "clear-search", defaultValue, hotkeyDisabled = false, - inputAriaLabel, - inputPaddingRight = 150, - inputTestId = "search-dags", onChange, placeholder, - startElement, ...props }: Props) => { const lastSentValue = useRef(defaultValue); @@ -111,8 +91,8 @@ export const SearchBar = ({ <> {Boolean(value) ? ( @@ -125,14 +105,13 @@ export const SearchBar = ({ )} } - startElement={startElement ?? } + startElement={} > diff --git a/airflow-core/src/airflow/ui/src/mocks/handlers/dags.ts b/airflow-core/src/airflow/ui/src/mocks/handlers/dags.ts index 92f2fa7d627d0..6a6116420ea76 100644 --- a/airflow-core/src/airflow/ui/src/mocks/handlers/dags.ts +++ b/airflow-core/src/airflow/ui/src/mocks/handlers/dags.ts @@ -127,6 +127,18 @@ const filterDags = ({ paused, timetableTypes }: { paused: string | null; timetab }; export const handlers: Array = [ + http.get("/ui/dags/timetable_types", ({ request }) => { + const url = new URL(request.url); + const prefix = url.searchParams.get("timetable_type_prefix_pattern") ?? ""; + const timetableTypes = ["CronTriggerTimetable", "NullTimetable"].filter((timetableType) => + timetableType.startsWith(prefix), + ); + + return HttpResponse.json({ + timetable_types: timetableTypes, + total_entries: timetableTypes.length, + }); + }), http.get("/ui/dags", ({ request }) => { const url = new URL(request.url); const lastDagRunState = url.searchParams.get("last_dag_run_state"); diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.test.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.test.tsx index 714a9ef234bec..f218000dae17b 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.test.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.test.tsx @@ -18,7 +18,7 @@ */ import "@testing-library/jest-dom"; import { fireEvent, render, screen, waitFor } from "@testing-library/react"; -import { describe, it, expect, vi } from "vitest"; +import { afterEach, describe, it, expect, vi } from "vitest"; import { AppWrapper } from "src/utils/AppWrapper"; @@ -39,6 +39,10 @@ vi.mock("src/queries/useConfig", () => ({ })); describe("Paused filter with hide_paused_dags_by_default enabled", () => { + afterEach(() => { + mockConfig.multi_team = false; + }); + it("defaults to showing only active dags", async () => { render(); @@ -68,19 +72,55 @@ describe("Paused filter with hide_paused_dags_by_default enabled", () => { await waitFor(() => expect(screen.queryByText("tutorial_taskflow_api_success")).not.toBeInTheDocument()); }); - it("filters dags by timetable type", async () => { + it("filters and clears dags by timetable type", async () => { render(); await waitFor(() => expect(screen.getByText("tutorial_taskflow_api_success")).toBeInTheDocument()); expect(screen.getByText("tutorial_taskflow_api_failed")).toBeInTheDocument(); - fireEvent.change(screen.getByTestId("dags-timetable-type-filter"), { - target: { value: "CronTriggerTimetable" }, + const timetableTypeFilter = screen.getByLabelText("filters.timetableType"); + + fireEvent.change(timetableTypeFilter, { target: { value: "Cron" } }); + const cronTriggerTimetable = await screen.findByText("CronTriggerTimetable"); + + expect(screen.queryByText("NullTimetable")).not.toBeInTheDocument(); + fireEvent.click(cronTriggerTimetable); + + await waitFor(() => { + expect(screen.queryByText("tutorial_taskflow_api_success")).not.toBeInTheDocument(); + expect(screen.getByText("tutorial_taskflow_api_failed")).toBeInTheDocument(); }); + fireEvent.keyDown(timetableTypeFilter, { code: "Backspace", key: "Backspace" }); + + await waitFor(() => { + expect(screen.getByText("tutorial_taskflow_api_success")).toBeInTheDocument(); + expect(screen.getByText("tutorial_taskflow_api_failed")).toBeInTheDocument(); + }); + }); + + it("restores a timetable type from the URL", async () => { + render(); + await waitFor(() => { expect(screen.queryByText("tutorial_taskflow_api_success")).not.toBeInTheDocument(); expect(screen.getByText("tutorial_taskflow_api_failed")).toBeInTheDocument(); }); + expect(screen.getByText("CronTriggerTimetable")).toBeInTheDocument(); + }); + + it("ignores an empty timetable type from the URL", async () => { + render(); + + await waitFor(() => expect(screen.getByText("tutorial_taskflow_api_success")).toBeInTheDocument()); + expect(screen.getByText("tutorial_taskflow_api_failed")).toBeInTheDocument(); + }); + + it("renders the team filter when multi-team is enabled", async () => { + mockConfig.multi_team = true; + + render(); + + expect(await screen.findByLabelText("dagDetails.team")).toBeInTheDocument(); }); }); diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx index 5927e9951ee3f..5fcfcdf4a0ff0 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx @@ -16,8 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import { HStack } from "@chakra-ui/react"; -import type { MultiValue } from "chakra-react-select"; +import { Box, HStack } from "@chakra-ui/react"; +import type { MultiValue, SingleValue } from "chakra-react-select"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useSearchParams } from "react-router-dom"; @@ -26,6 +26,7 @@ import { useTableURLState } from "src/components/DataTable/useTableUrlState"; import { SearchParamsKeys, type SearchParamsKeysType } from "src/constants/searchParams"; import { useConfig } from "src/queries/useConfig"; import { useDagTagsInfinite } from "src/queries/useDagTagsInfinite"; +import { useDagTimetableTypesInfinite } from "src/queries/useDagTimetableTypesInfinite"; import { useTagFilter } from "../useTagFilter"; import { FavoriteFilter } from "./FavoriteFilter"; @@ -72,12 +73,25 @@ export const DagsFilters = () => { const selectedTeams = searchParams.getAll(TEAMS_PARAM); const timetableType = searchParams.get(TIMETABLE_TYPE_PARAM) ?? ""; - const [pattern, setPattern] = useState(""); + const [tagPattern, setTagPattern] = useState(""); + const [timetableTypePattern, setTimetableTypePattern] = useState(""); - const { data, fetchNextPage, fetchPreviousPage } = useDagTagsInfinite({ + const { + data: tagData, + fetchNextPage: fetchNextTagPage, + fetchPreviousPage: fetchPreviousTagPage, + } = useDagTagsInfinite({ limit: 10, orderBy: ["name"], - tagNamePattern: pattern, + tagNamePattern: tagPattern, + }); + const { + data: timetableTypeData, + fetchNextPage: fetchNextTimetableTypePage, + fetchPreviousPage: fetchPreviousTimetableTypePage, + } = useDagTimetableTypesInfinite({ + limit: 10, + timetableTypePrefixPattern: timetableTypePattern, }); const hidePausedDagsByDefault = Boolean(useConfig("hide_paused_dags_by_default")); @@ -144,9 +158,11 @@ export const DagsFilters = () => { setSearchParams(searchParams); }; - const handleTimetableTypeChange = (value: string) => { - if (value) { - searchParams.set(TIMETABLE_TYPE_PARAM, value); + const handleTimetableTypeChange = ( + selectedTimetableType: SingleValue<{ label: string; value: string }>, + ) => { + if (selectedTimetableType) { + searchParams.set(TIMETABLE_TYPE_PARAM, selectedTimetableType.value); } else { searchParams.delete(TIMETABLE_TYPE_PARAM); } @@ -180,7 +196,7 @@ export const DagsFilters = () => { const favoriteValue = toBooleanFilterValue(showFavorites); return ( - + { /> - + { + void fetchNextTimetableTypePage(); + }} + onMenuScrollToTop={() => { + void fetchPreviousTimetableTypePage(); + }} + timetableTypes={timetableTypeData?.pages.flatMap((response) => response.timetable_types) ?? []} + value={timetableType || undefined} + /> { - void fetchNextPage(); + void fetchNextTagPage(); }} onMenuScrollToTop={() => { - void fetchPreviousPage(); + void fetchPreviousTagPage(); }} onSelectTagsChange={handleSelectTagsChange} onTagModeChange={handleTagModeChange} - onUpdate={setPattern} + onUpdate={setTagPattern} selectedTags={selectedTags} tagFilterMode={tagFilterMode} - tags={data?.pages.flatMap((dagResponse) => dagResponse.tags) ?? []} + tags={tagData?.pages.flatMap((dagResponse) => dagResponse.tags) ?? []} /> - {multiTeamEnabled ? ( ) : undefined} + + + ); }; diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TagFilter.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TagFilter.tsx index f40d5829e3237..981e05b521354 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TagFilter.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TagFilter.tsx @@ -46,7 +46,7 @@ export const TagFilter = ({ const { t: translate } = useTranslation("common"); return ( - + ({ ...provided, - maxWidth: 300, - minWidth: 64, + width: "100%", }), control: (provided) => ({ ...provided, diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TeamFilter.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TeamFilter.tsx index 3b315aa83173f..469503b09aea2 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TeamFilter.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TeamFilter.tsx @@ -41,7 +41,7 @@ export const TeamFilter = ({ onChange, selectedTeams }: Props) => { }; return ( - + { }), container: (provided) => ({ ...provided, - maxWidth: 200, - minWidth: 64, + width: "100%", }), control: (provided) => ({ ...provided, diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TimetableTypeFilter.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TimetableTypeFilter.tsx index a7b3a93cb2be6..88bacd9b53fda 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TimetableTypeFilter.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TimetableTypeFilter.tsx @@ -16,33 +16,67 @@ * specific language governing permissions and limitations * under the License. */ +import { Box, Field } from "@chakra-ui/react"; +import { Select as ReactSelect, type SingleValue } from "chakra-react-select"; import { useTranslation } from "react-i18next"; -import { FiClock } from "react-icons/fi"; - -import { SearchBar } from "src/components/SearchBar"; type Props = { - readonly onChange: (value: string) => void; - readonly value: string; + readonly onChange: (timetableType: SingleValue) => void; + readonly onInputChange: (value: string) => void; + readonly onMenuScrollToBottom: () => void; + readonly onMenuScrollToTop: () => void; + readonly timetableTypes: Array; + readonly value: string | undefined; +}; + +type TimetableTypeOption = { + label: string; + value: string; }; -export const TimetableTypeFilter = ({ onChange, value: initialValue }: Props) => { +export const TimetableTypeFilter = ({ + onChange, + onInputChange, + onMenuScrollToBottom, + onMenuScrollToTop, + timetableTypes, + value, +}: Props) => { const { t: translate } = useTranslation("dags"); return ( - onChange(newValue.trim())} - placeholder={translate("filters.timetableType")} - startElement={} - /> + + + + aria-label={translate("filters.timetableType")} + chakraStyles={{ + clearIndicator: (provided) => ({ + ...provided, + color: "gray.fg", + }), + control: (provided) => ({ + ...provided, + colorPalette: "brand", + }), + menu: (provided) => ({ + ...provided, + zIndex: 2, + }), + }} + isClearable + noOptionsMessage={() => translate("filters.noTimetableTypesFound")} + onChange={onChange} + onInputChange={onInputChange} + onMenuScrollToBottom={onMenuScrollToBottom} + onMenuScrollToTop={onMenuScrollToTop} + options={timetableTypes.map((timetableType) => ({ + label: timetableType, + value: timetableType, + }))} + placeholder={translate("filters.timetableType")} + value={value === undefined ? null : { label: value, value }} + /> + + ); }; diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx index f99ee95980a18..5faaa5321ae88 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx @@ -260,7 +260,7 @@ export const DagsList = () => { const pendingReviews = searchParams.get(NEEDS_REVIEW); const owners = searchParams.getAll(OWNERS); const teams = searchParams.getAll(TEAMS); - const timetableType = searchParams.getAll(TIMETABLE_TYPE); + const timetableType = searchParams.getAll(TIMETABLE_TYPE).filter((value) => value !== ""); const { setTableURLState, tableURLState } = useTableURLState(); diff --git a/airflow-core/src/airflow/ui/src/queries/useDagTimetableTypesInfinite.ts b/airflow-core/src/airflow/ui/src/queries/useDagTimetableTypesInfinite.ts new file mode 100644 index 0000000000000..17fa4355a9a22 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/queries/useDagTimetableTypesInfinite.ts @@ -0,0 +1,68 @@ +/*! + * 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 { type InfiniteData, useInfiniteQuery, type UseInfiniteQueryOptions } from "@tanstack/react-query"; + +import { UseDagServiceGetDagTimetableTypesUiKeyFn } from "openapi/queries"; +import { DagService } from "openapi/requests/services.gen"; +import type { DagTimetableTypeCollectionResponse } from "openapi/requests/types.gen"; + +export const useDagTimetableTypesInfinite = ( + { + limit, + timetableTypePrefixPattern, + }: { + limit?: number; + timetableTypePrefixPattern?: string; + } = {}, + queryKey?: Array, + options?: Omit< + UseInfiniteQueryOptions< + DagTimetableTypeCollectionResponse, + TError, + InfiniteData, + Array, + number + >, + "queryFn" | "queryKey" + >, +) => + useInfiniteQuery({ + getNextPageParam: ( + lastPage: DagTimetableTypeCollectionResponse, + _allPages: Array, + lastPageParam: number, + ) => + lastPageParam + lastPage.timetable_types.length < lastPage.total_entries + ? lastPageParam + lastPage.timetable_types.length + : undefined, + getPreviousPageParam: ( + firstPage: DagTimetableTypeCollectionResponse, + _allPages: Array, + firstPageParam: number, + ) => (firstPageParam > 0 ? Math.max(0, firstPageParam - firstPage.timetable_types.length) : undefined), + initialPageParam: 0, + queryFn: ({ pageParam }: { pageParam: number }) => + DagService.getDagTimetableTypesUi({ + limit, + offset: pageParam, + timetableTypePrefixPattern, + }), + queryKey: UseDagServiceGetDagTimetableTypesUiKeyFn({ limit, timetableTypePrefixPattern }, queryKey), + ...options, + }); diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dags.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dags.py index 4b86ef251c577..14c7c73f657e9 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dags.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dags.py @@ -510,6 +510,85 @@ def test_is_favorite_field_user_specific(self, test_client, session): _PARENT_DAG3_SUCCESS = 1 # via ``_create_deactivated_paused_dag`` +class TestGetDagTimetableTypes(TestPublicDagEndpoint): + @pytest.fixture(autouse=True) + def setup_timetable_types(self, session): + session.get(DagModel, DAG1_ID).timetable_type = "CronTriggerTimetable" + session.get(DagModel, DAG2_ID).timetable_type = "example.CustomTimetable" + session.get(DagModel, DAG3_ID).timetable_type = "StaleOnlyTimetable" + session.commit() + + @pytest.mark.parametrize( + ("query_params", "expected_types", "expected_total_entries"), + [ + ({}, ["CronTriggerTimetable", "example.CustomTimetable"], 2), + ({"limit": 1}, ["CronTriggerTimetable"], 2), + ({"offset": 1}, ["example.CustomTimetable"], 2), + ( + {"timetable_type_prefix_pattern": "example"}, + ["example.CustomTimetable"], + 1, + ), + ({"timetable_type_prefix_pattern": "missing"}, [], 0), + ], + ) + def test_get_dag_timetable_types( + self, + test_client, + query_params, + expected_types, + expected_total_entries, + ): + response = test_client.get("/dags/timetable_types", params=query_params) + + assert response.status_code == 200 + assert response.json() == { + "timetable_types": expected_types, + "total_entries": expected_total_entries, + } + + @mock.patch("airflow.api_fastapi.auth.managers.base_auth_manager.BaseAuthManager.get_authorized_dag_ids") + def test_only_returns_types_from_readable_dags( + self, + mock_get_authorized_dag_ids, + test_client, + ): + mock_get_authorized_dag_ids.return_value = {DAG2_ID} + + response = test_client.get("/dags/timetable_types") + + mock_get_authorized_dag_ids.assert_called_once_with(user=mock.ANY, method="GET") + assert response.status_code == 200 + assert response.json() == { + "timetable_types": ["example.CustomTimetable"], + "total_entries": 1, + } + + def test_excludes_blank_timetable_types(self, test_client, session): + session.get(DagModel, DAG1_ID).timetable_type = "" + session.commit() + + response = test_client.get("/dags/timetable_types") + + assert response.status_code == 200 + assert response.json() == { + "timetable_types": ["example.CustomTimetable"], + "total_entries": 1, + } + + def test_returns_distinct_timetable_types(self, test_client, session): + session.get(DagModel, DAG2_ID).timetable_type = "CronTriggerTimetable" + session.commit() + + response = test_client.get("/dags/timetable_types") + + assert response.status_code == 200 + assert response.json() == { + "timetable_types": ["CronTriggerTimetable"], + "total_entries": 1, + } + + class TestGetDagRunStateCounts(TestPublicDagEndpoint): """Tests for ``GET /ui/dags/run_state_counts``.""" From 91911be63d571341992098d5bc1a56044214bdf2 Mon Sep 17 00:00:00 2001 From: Shivam Rastogi <6463385+shivaam@users.noreply.github.com> Date: Sat, 25 Jul 2026 15:12:03 -0700 Subject: [PATCH 4/4] UI: Reuse multi-select for Dag filters Allow selecting multiple timetable types with OR semantics and share the tag-style select presentation between tag and timetable filters. --- .../ui/openapi-gen/requests/services.gen.ts | 4 +- .../DagsList/DagsFilters/DagsFilterSelect.tsx | 83 +++++++++++++++++++ .../DagsList/DagsFilters/DagsFilters.test.tsx | 28 ++++++- .../DagsList/DagsFilters/DagsFilters.tsx | 25 ++---- .../pages/DagsList/DagsFilters/TagFilter.tsx | 57 ++++--------- .../DagsFilters/TimetableTypeFilter.tsx | 58 ++++--------- 6 files changed, 149 insertions(+), 106 deletions(-) create mode 100644 airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilterSelect.tsx diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts index 49d06a820dc67..10cc6fff1c6c6 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts @@ -2043,7 +2043,7 @@ export class DagService { } }); } - + /** * Get Latest Run Info * Get latest run. @@ -5282,4 +5282,4 @@ export class TeamsService { }); } -} +} \ No newline at end of file diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilterSelect.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilterSelect.tsx new file mode 100644 index 0000000000000..3a6858fdb9a66 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilterSelect.tsx @@ -0,0 +1,83 @@ +/*! + * 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 { Field } from "@chakra-ui/react"; +import { Select as ReactSelect } from "chakra-react-select"; + +type FilterOption = { + label: string; + value: string; +}; + +type Props = { + readonly ariaLabel: string; + readonly noOptionsMessage: string; + readonly onChange: (values: Array) => void; + readonly onInputChange?: (value: string) => void; + readonly onMenuScrollToBottom?: () => void; + readonly onMenuScrollToTop?: () => void; + readonly options: Array; + readonly placeholder: string; + readonly values: Array; +}; + +export const DagsFilterSelect = ({ + ariaLabel, + noOptionsMessage, + onChange, + onInputChange, + onMenuScrollToBottom, + onMenuScrollToTop, + options, + placeholder, + values, +}: Props) => ( + + + aria-label={ariaLabel} + chakraStyles={{ + clearIndicator: (provided) => ({ + ...provided, + color: "gray.fg", + }), + container: (provided) => ({ + ...provided, + width: "100%", + }), + control: (provided) => ({ + ...provided, + colorPalette: "brand", + }), + menu: (provided) => ({ + ...provided, + zIndex: 2, + }), + }} + isClearable + isMulti + noOptionsMessage={() => noOptionsMessage} + onChange={(selected) => onChange(selected.map(({ value }) => value))} + onInputChange={onInputChange} + onMenuScrollToBottom={onMenuScrollToBottom} + onMenuScrollToTop={onMenuScrollToTop} + options={options.map((option) => ({ label: option, value: option }))} + placeholder={placeholder} + value={values.map((value) => ({ label: value, value }))} + /> + +); diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.test.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.test.tsx index f218000dae17b..14cdfa4c212cf 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.test.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.test.tsx @@ -72,7 +72,7 @@ describe("Paused filter with hide_paused_dags_by_default enabled", () => { await waitFor(() => expect(screen.queryByText("tutorial_taskflow_api_success")).not.toBeInTheDocument()); }); - it("filters and clears dags by timetable type", async () => { + it("filters and clears dags by timetable types", async () => { render(); await waitFor(() => expect(screen.getByText("tutorial_taskflow_api_success")).toBeInTheDocument()); @@ -91,6 +91,21 @@ describe("Paused filter with hide_paused_dags_by_default enabled", () => { expect(screen.getByText("tutorial_taskflow_api_failed")).toBeInTheDocument(); }); + fireEvent.change(timetableTypeFilter, { target: { value: "Null" } }); + fireEvent.click(await screen.findByText("NullTimetable")); + + await waitFor(() => { + expect(screen.getByText("tutorial_taskflow_api_success")).toBeInTheDocument(); + expect(screen.getByText("tutorial_taskflow_api_failed")).toBeInTheDocument(); + }); + + fireEvent.keyDown(timetableTypeFilter, { code: "Backspace", key: "Backspace" }); + + await waitFor(() => { + expect(screen.queryByText("tutorial_taskflow_api_success")).not.toBeInTheDocument(); + expect(screen.getByText("tutorial_taskflow_api_failed")).toBeInTheDocument(); + }); + fireEvent.keyDown(timetableTypeFilter, { code: "Backspace", key: "Backspace" }); await waitFor(() => { @@ -99,14 +114,19 @@ describe("Paused filter with hide_paused_dags_by_default enabled", () => { }); }); - it("restores a timetable type from the URL", async () => { - render(); + it("restores timetable types from the URL", async () => { + render( + , + ); await waitFor(() => { - expect(screen.queryByText("tutorial_taskflow_api_success")).not.toBeInTheDocument(); + expect(screen.getByText("tutorial_taskflow_api_success")).toBeInTheDocument(); expect(screen.getByText("tutorial_taskflow_api_failed")).toBeInTheDocument(); }); expect(screen.getByText("CronTriggerTimetable")).toBeInTheDocument(); + expect(screen.getByText("NullTimetable")).toBeInTheDocument(); }); it("ignores an empty timetable type from the URL", async () => { diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx index 5fcfcdf4a0ff0..7480a064692fa 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx @@ -17,7 +17,6 @@ * under the License. */ import { Box, HStack } from "@chakra-ui/react"; -import type { MultiValue, SingleValue } from "chakra-react-select"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useSearchParams } from "react-router-dom"; @@ -71,7 +70,7 @@ export const DagsFilters = () => { const state = searchParams.get(LAST_DAG_RUN_STATE_PARAM); const activeRunState = searchParams.get(DAG_RUN_STATE_PARAM); const selectedTeams = searchParams.getAll(TEAMS_PARAM); - const timetableType = searchParams.get(TIMETABLE_TYPE_PARAM) ?? ""; + const timetableTypes = searchParams.getAll(TIMETABLE_TYPE_PARAM).filter(Boolean); const [tagPattern, setTagPattern] = useState(""); const [timetableTypePattern, setTimetableTypePattern] = useState(""); @@ -158,25 +157,17 @@ export const DagsFilters = () => { setSearchParams(searchParams); }; - const handleTimetableTypeChange = ( - selectedTimetableType: SingleValue<{ label: string; value: string }>, - ) => { - if (selectedTimetableType) { - searchParams.set(TIMETABLE_TYPE_PARAM, selectedTimetableType.value); - } else { - searchParams.delete(TIMETABLE_TYPE_PARAM); + const handleTimetableTypeChange = (selectedTimetableTypes: Array) => { + searchParams.delete(TIMETABLE_TYPE_PARAM); + for (const timetableType of selectedTimetableTypes) { + searchParams.append(TIMETABLE_TYPE_PARAM, timetableType); } resetPagination(); setSearchParams(searchParams); }; - const handleSelectTagsChange = ( - tags: MultiValue<{ - label: string; - value: string; - }>, - ) => { - setSelectedTags(tags.map(({ value }) => value)); + const handleSelectTagsChange = (tags: Array) => { + setSelectedTags(tags); }; const handleTagModeChange = ({ checked }: { checked: boolean }) => { @@ -223,7 +214,7 @@ export const DagsFilters = () => { void fetchPreviousTimetableTypePage(); }} timetableTypes={timetableTypeData?.pages.flatMap((response) => response.timetable_types) ?? []} - value={timetableType || undefined} + values={timetableTypes} /> { diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TagFilter.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TagFilter.tsx index 981e05b521354..c50a3361babd8 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TagFilter.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TagFilter.tsx @@ -16,16 +16,17 @@ * specific language governing permissions and limitations * under the License. */ -import { Box, Field, HStack, Text } from "@chakra-ui/react"; -import { Select as ReactSelect, type MultiValue } from "chakra-react-select"; +import { Box, HStack, Text } from "@chakra-ui/react"; import { useTranslation } from "react-i18next"; import { Switch } from "src/components/ui"; +import { DagsFilterSelect } from "./DagsFilterSelect"; + type Props = { readonly onMenuScrollToBottom: () => void; readonly onMenuScrollToTop: () => void; - readonly onSelectTagsChange: (tags: MultiValue<{ label: string; value: string }>) => void; + readonly onSelectTagsChange: (tags: Array) => void; readonly onTagModeChange: ({ checked }: { checked: boolean }) => void; readonly onUpdate: (newValue: string) => void; readonly selectedTags: Array; @@ -47,45 +48,17 @@ export const TagFilter = ({ return ( - - ({ - ...provided, - color: "gray.fg", - }), - container: (provided) => ({ - ...provided, - width: "100%", - }), - control: (provided) => ({ - ...provided, - colorPalette: "brand", - }), - menu: (provided) => ({ - ...provided, - zIndex: 2, - }), - }} - isClearable - isMulti - noOptionsMessage={() => translate("table.noTagsFound")} - onChange={onSelectTagsChange} - onInputChange={(newValue) => onUpdate(newValue)} - onMenuScrollToBottom={onMenuScrollToBottom} - onMenuScrollToTop={onMenuScrollToTop} - options={tags.map((tag) => ({ - label: tag, - value: tag, - }))} - placeholder={translate("table.tagPlaceholder")} - value={selectedTags.map((tag) => ({ - label: tag, - value: tag, - }))} - /> - + {selectedTags.length >= 2 && ( ) => void; + readonly onChange: (timetableTypes: Array) => void; readonly onInputChange: (value: string) => void; readonly onMenuScrollToBottom: () => void; readonly onMenuScrollToTop: () => void; readonly timetableTypes: Array; - readonly value: string | undefined; -}; - -type TimetableTypeOption = { - label: string; - value: string; + readonly values: Array; }; export const TimetableTypeFilter = ({ @@ -40,43 +36,23 @@ export const TimetableTypeFilter = ({ onMenuScrollToBottom, onMenuScrollToTop, timetableTypes, - value, + values, }: Props) => { const { t: translate } = useTranslation("dags"); return ( - - - aria-label={translate("filters.timetableType")} - chakraStyles={{ - clearIndicator: (provided) => ({ - ...provided, - color: "gray.fg", - }), - control: (provided) => ({ - ...provided, - colorPalette: "brand", - }), - menu: (provided) => ({ - ...provided, - zIndex: 2, - }), - }} - isClearable - noOptionsMessage={() => translate("filters.noTimetableTypesFound")} - onChange={onChange} - onInputChange={onInputChange} - onMenuScrollToBottom={onMenuScrollToBottom} - onMenuScrollToTop={onMenuScrollToTop} - options={timetableTypes.map((timetableType) => ({ - label: timetableType, - value: timetableType, - }))} - placeholder={translate("filters.timetableType")} - value={value === undefined ? null : { label: value, value }} - /> - + ); };