From 8b2b17d530d331a0459c5d512eecc5296eced083 Mon Sep 17 00:00:00 2001 From: Brent Bovenzi Date: Fri, 9 Jan 2026 11:12:53 -0500 Subject: [PATCH 1/3] Fix colors and remove reset all --- .../DataTable/ToggleTableDisplay.tsx | 4 +- .../DagsList/DagsFilters/DagsFilters.tsx | 93 +++++++------------ .../DagsList/DagsFilters/FavoriteFilter.tsx | 10 +- .../DagsList/DagsFilters/PausedFilter.tsx | 6 +- .../DagsList/DagsFilters/StateFilters.tsx | 12 +-- .../pages/DagsList/DagsFilters/TagFilter.tsx | 6 +- 6 files changed, 51 insertions(+), 80 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/components/DataTable/ToggleTableDisplay.tsx b/airflow-core/src/airflow/ui/src/components/DataTable/ToggleTableDisplay.tsx index 75e336d615f19..0c0b6e4ab99d8 100644 --- a/airflow-core/src/airflow/ui/src/components/DataTable/ToggleTableDisplay.tsx +++ b/airflow-core/src/airflow/ui/src/components/DataTable/ToggleTableDisplay.tsx @@ -34,7 +34,7 @@ export const ToggleTableDisplay = ({ display, setDisplay }: Props) => { setDisplay("card")} title={translate("toggleCardView")} variant={display === "card" ? "solid" : "outline"} @@ -43,7 +43,7 @@ export const ToggleTableDisplay = ({ display, setDisplay }: Props) => { setDisplay("table")} title={translate("toggleTableView")} variant={display === "table" ? "solid" : "outline"} 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 7bac1898496d0..79a512afab439 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,17 +16,15 @@ * specific language governing permissions and limitations * under the License. */ -import { Box, Flex } from "@chakra-ui/react"; +import { HStack } from "@chakra-ui/react"; import type { MultiValue } from "chakra-react-select"; import { useState } from "react"; import { useSearchParams } from "react-router-dom"; import { useTableURLState } from "src/components/DataTable/useTableUrlState"; -import { ResetButton } from "src/components/ui"; import { SearchParamsKeys, type SearchParamsKeysType } from "src/constants/searchParams"; import { useConfig } from "src/queries/useConfig"; import { useDagTagsInfinite } from "src/queries/useDagTagsInfinite"; -import { getFilterCount } from "src/utils/filterUtils"; import { FavoriteFilter } from "./FavoriteFilter"; import { PausedFilter } from "./PausedFilter"; @@ -142,18 +140,6 @@ export const DagsFilters = () => { setSearchParams(searchParams); }; - const onClearFilters = () => { - searchParams.delete(PAUSED_PARAM); - searchParams.delete(FAVORITE_PARAM); - searchParams.delete(NEEDS_REVIEW_PARAM); - searchParams.delete(LAST_DAG_RUN_STATE_PARAM); - searchParams.delete(TAGS_PARAM); - searchParams.delete(TAGS_MATCH_MODE_PARAM); - - setSearchParams(searchParams); - setPattern(""); - }; - const handleTagModeChange = ({ checked }: { checked: boolean }) => { const mode = checked ? "all" : "any"; @@ -161,52 +147,37 @@ export const DagsFilters = () => { setSearchParams(searchParams); }; - const filterCount = getFilterCount({ - needsReview, - selectedTags, - showFavorites, - showPaused, - state, - }); - return ( - - - - - - { - void fetchNextPage(); - }} - onMenuScrollToTop={() => { - void fetchPreviousPage(); - }} - onSelectTagsChange={handleSelectTagsChange} - onTagModeChange={handleTagModeChange} - onUpdate={setPattern} - selectedTags={selectedTags} - tagFilterMode={tagFilterMode} - tags={data?.pages.flatMap((dagResponse) => dagResponse.tags) ?? []} - /> - - - - - - - + + + + { + void fetchNextPage(); + }} + onMenuScrollToTop={() => { + void fetchPreviousPage(); + }} + onSelectTagsChange={handleSelectTagsChange} + onTagModeChange={handleTagModeChange} + onUpdate={setPattern} + selectedTags={selectedTags} + tagFilterMode={tagFilterMode} + tags={data?.pages.flatMap((dagResponse) => dagResponse.tags) ?? []} + /> + + ); }; diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/FavoriteFilter.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/FavoriteFilter.tsx index 3554658f98dae..51841951ec115 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/FavoriteFilter.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/FavoriteFilter.tsx @@ -33,7 +33,7 @@ export const FavoriteFilter = ({ onFavoriteChange, showFavorites }: Props) => { return ( + ); + })} + +); diff --git a/airflow-core/src/airflow/ui/src/components/ui/index.ts b/airflow-core/src/airflow/ui/src/components/ui/index.ts index b2db1b811b0ce..d7ce843b1192b 100644 --- a/airflow-core/src/airflow/ui/src/components/ui/index.ts +++ b/airflow-core/src/airflow/ui/src/components/ui/index.ts @@ -33,3 +33,4 @@ export * from "./Popover"; export * from "./Checkbox"; export * from "./ResetButton"; export * from "./InputWithAddon"; +export * from "./ButtonGroupToggle"; 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 79a512afab439..3a1ee4fdf5819 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 @@ -28,6 +28,7 @@ import { useDagTagsInfinite } from "src/queries/useDagTagsInfinite"; import { FavoriteFilter } from "./FavoriteFilter"; import { PausedFilter } from "./PausedFilter"; +import { RequiredActionFilter } from "./RequiredActionFilter"; import { StateFilters } from "./StateFilters"; import { TagFilter } from "./TagFilter"; @@ -41,6 +42,21 @@ const { TAGS_MATCH_MODE: TAGS_MATCH_MODE_PARAM, }: SearchParamsKeysType = SearchParamsKeys; +type StateValue = "all" | "failed" | "queued" | "running" | "success"; +type BooleanFilterValue = "all" | "false" | "true"; + +const stateValues: ReadonlyArray = ["failed", "queued", "running", "success"]; +const booleanFilterValues: ReadonlyArray = ["all", "true", "false"]; + +const toStateValue = (value: string | null): StateValue => + stateValues.includes(value as StateValue) ? (value as StateValue) : "all"; + +const toBooleanFilterValue = ( + value: string | null, + defaultValue: BooleanFilterValue = "all", +): BooleanFilterValue => + booleanFilterValues.includes(value as BooleanFilterValue) ? (value as BooleanFilterValue) : defaultValue; + export const DagsFilters = () => { const [searchParams, setSearchParams] = useSearchParams(); @@ -50,11 +66,6 @@ export const DagsFilters = () => { const state = searchParams.get(LAST_DAG_RUN_STATE_PARAM); const selectedTags = searchParams.getAll(TAGS_PARAM); const tagFilterMode = searchParams.get(TAGS_MATCH_MODE_PARAM) ?? "any"; - const isAll = state === null; - const isRunning = state === "running"; - const isFailed = state === "failed"; - const isQueued = state === "queued"; - const isSuccess = state === "success"; const [pattern, setPattern] = useState(""); @@ -65,61 +76,56 @@ export const DagsFilters = () => { }); const hidePausedDagsByDefault = Boolean(useConfig("hide_paused_dags_by_default")); - const defaultShowPaused = hidePausedDagsByDefault ? "false" : "all"; + const defaultShowPaused: BooleanFilterValue = hidePausedDagsByDefault ? "false" : "all"; const { setTableURLState, tableURLState } = useTableURLState(); const { pagination, sorting } = tableURLState; - const handlePausedChange: React.MouseEventHandler = ({ currentTarget: { value } }) => { - if (value === "all") { - searchParams.delete(PAUSED_PARAM); - } else { - searchParams.set(PAUSED_PARAM, value); - } + const resetPagination = () => { setTableURLState({ pagination: { ...pagination, pageIndex: 0 }, sorting, }); searchParams.delete(OFFSET_PARAM); + }; + + const handlePausedChange = (value: BooleanFilterValue) => { + if (value === "all") { + searchParams.delete(PAUSED_PARAM); + } else { + searchParams.set(PAUSED_PARAM, value); + } + resetPagination(); setSearchParams(searchParams); }; - const handleFavoriteChange: React.MouseEventHandler = ({ currentTarget: { value } }) => { + const handleFavoriteChange = (value: BooleanFilterValue) => { if (value === "all") { searchParams.delete(FAVORITE_PARAM); } else { searchParams.set(FAVORITE_PARAM, value); } - setTableURLState({ - pagination: { ...pagination, pageIndex: 0 }, - sorting, - }); - searchParams.delete(OFFSET_PARAM); + resetPagination(); setSearchParams(searchParams); }; - const handleStateChange: React.MouseEventHandler = ({ currentTarget: { value } }) => { + const handleStateChange = (value: StateValue) => { if (value === "all") { searchParams.delete(LAST_DAG_RUN_STATE_PARAM); + } else { + searchParams.set(LAST_DAG_RUN_STATE_PARAM, value); + } + resetPagination(); + setSearchParams(searchParams); + }; + + const handleNeedsReviewToggle = () => { + if (needsReview === "true") { searchParams.delete(NEEDS_REVIEW_PARAM); - } else if (value === "needs_review") { - if (needsReview === "true") { - searchParams.delete(NEEDS_REVIEW_PARAM); - } else { - searchParams.set(NEEDS_REVIEW_PARAM, "true"); - } } else { - if (state === value) { - searchParams.delete(LAST_DAG_RUN_STATE_PARAM); - } else { - searchParams.set(LAST_DAG_RUN_STATE_PARAM, value); - } + searchParams.set(NEEDS_REVIEW_PARAM, "true"); } - setTableURLState({ - pagination: { ...pagination, pageIndex: 0 }, - sorting, - }); - searchParams.delete(OFFSET_PARAM); + resetPagination(); setSearchParams(searchParams); }; @@ -147,22 +153,15 @@ export const DagsFilters = () => { setSearchParams(searchParams); }; + const stateValue = toStateValue(state); + const pausedValue = toBooleanFilterValue(showPaused, defaultShowPaused); + const favoriteValue = toBooleanFilterValue(showFavorites); + return ( - - + + + { void fetchNextPage(); @@ -177,7 +176,7 @@ export const DagsFilters = () => { tagFilterMode={tagFilterMode} tags={data?.pages.flatMap((dagResponse) => dagResponse.tags) ?? []} /> - + ); }; diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/FavoriteFilter.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/FavoriteFilter.tsx index 51841951ec115..71f0c0e893a56 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/FavoriteFilter.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/FavoriteFilter.tsx @@ -16,55 +16,52 @@ * specific language governing permissions and limitations * under the License. */ -import { Button, ButtonGroup, Icon } from "@chakra-ui/react"; +import { Icon } from "@chakra-ui/react"; import { useTranslation } from "react-i18next"; import { FiStar } from "react-icons/fi"; +import { ButtonGroupToggle } from "src/components/ui/ButtonGroupToggle"; + +type FavoriteValue = "all" | "false" | "true"; + type Props = { - readonly onFavoriteChange: React.MouseEventHandler; - readonly showFavorites: string | null; + readonly onChange: (value: FavoriteValue) => void; + readonly value: FavoriteValue; }; -export const FavoriteFilter = ({ onFavoriteChange, showFavorites }: Props) => { - const { t: translate } = useTranslation("dags"); +const StarIcon = ({ filled, isSelected }: { readonly filled: boolean; readonly isSelected: boolean }) => ( + + + +); - const currentValue = showFavorites ?? "all"; +const renderFavoriteLabel = + (text: string, filled: boolean) => + (isSelected: boolean): React.ReactNode => ( + <> + + {text} + + ); + +export const FavoriteFilter = ({ onChange, value }: Props) => { + const { t: translate } = useTranslation("dags"); return ( - - - - - + + onChange={onChange} + options={[ + { label: translate("filters.favorite.all"), value: "all" }, + { + label: renderFavoriteLabel(translate("filters.favorite.favorite"), true), + value: "true", + }, + { + label: renderFavoriteLabel(translate("filters.favorite.unfavorite"), false), + value: "false", + }, + ]} + value={value} + /> ); }; diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/PausedFilter.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/PausedFilter.tsx index 43a98ba234746..bd02ac68bfa48 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/PausedFilter.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/PausedFilter.tsx @@ -16,49 +16,25 @@ * specific language governing permissions and limitations * under the License. */ -import { Button, ButtonGroup } from "@chakra-ui/react"; import { useTranslation } from "react-i18next"; +import { ButtonGroupToggle } from "src/components/ui"; + +type PausedValue = "all" | "false" | "true"; + type Props = { - readonly defaultShowPaused: string; - readonly onPausedChange: React.MouseEventHandler; - readonly showPaused: string | null; + readonly onChange: (value: PausedValue) => void; + readonly value: PausedValue; }; -export const PausedFilter = ({ defaultShowPaused, onPausedChange, showPaused }: Props) => { +export const PausedFilter = ({ onChange, value }: Props) => { const { t: translate } = useTranslation("dags"); - const currentValue = showPaused ?? defaultShowPaused; + const options = [ + { label: translate("filters.paused.all"), value: "all" as const }, + { label: translate("filters.paused.active"), value: "false" as const }, + { label: translate("filters.paused.paused"), value: "true" as const }, + ]; - return ( - - - - - - ); + return onChange={onChange} options={options} value={value} />; }; diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/RequiredActionFilter.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/RequiredActionFilter.tsx new file mode 100644 index 0000000000000..b061a41da0793 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/RequiredActionFilter.tsx @@ -0,0 +1,48 @@ +/*! + * 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 { Button } from "@chakra-ui/react"; +import { useTranslation } from "react-i18next"; +import { LuUserRoundPen } from "react-icons/lu"; + +import { StateBadge } from "src/components/StateBadge"; + +type Props = { + readonly needsReview: boolean; + readonly onToggle: () => void; +}; + +export const RequiredActionFilter = ({ needsReview, onToggle }: Props) => { + const { t: translate } = useTranslation("hitl"); + + return ( + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/StateFilters.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/StateFilters.tsx index ddf73bd85ad10..47dc79953b891 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/StateFilters.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/StateFilters.tsx @@ -16,101 +16,60 @@ * specific language governing permissions and limitations * under the License. */ -import { Button, ButtonGroup } from "@chakra-ui/react"; import { useTranslation } from "react-i18next"; -import { LuUserRoundPen } from "react-icons/lu"; import { StateBadge } from "src/components/StateBadge"; +import { ButtonGroupToggle, type ButtonGroupOption } from "src/components/ui/ButtonGroupToggle"; + +type StateValue = "all" | "failed" | "queued" | "running" | "success"; type Props = { - readonly isAll: boolean; - readonly isFailed: boolean; - readonly isQueued: boolean; - readonly isRunning: boolean; - readonly isSuccess: boolean; - readonly needsReview: boolean; - readonly onStateChange: React.MouseEventHandler; + readonly onChange: (value: StateValue) => void; + readonly value: StateValue; }; -export const StateFilters = ({ - isAll, - isFailed, - isQueued, - isRunning, - isSuccess, - needsReview, - onStateChange, -}: Props) => { - const { t: translate } = useTranslation(["dags", "common", "hitl"]); +export const StateFilters = ({ onChange, value }: Props) => { + const { t: translate } = useTranslation(["dags", "common"]); + + const options: Array> = [ + { label: translate("dags:filters.paused.all"), value: "all" }, + { + label: ( + <> + + {translate("common:states.failed")} + + ), + value: "failed", + }, + { + label: ( + <> + + {translate("common:states.queued")} + + ), + value: "queued", + }, + { + label: ( + <> + + {translate("common:states.running")} + + ), + value: "running", + }, + { + label: ( + <> + + {translate("common:states.success")} + + ), + value: "success", + }, + ]; - return ( - - - - - - - - - ); + return onChange={onChange} options={options} value={value} />; }; diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.test.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.test.tsx index 39608f4a7452a..1f840cc5d40af 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.test.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.test.tsx @@ -26,12 +26,12 @@ describe("Dag Filters", () => { it("Filter by selected last run state", async () => { render(); - await waitFor(() => expect(screen.getByTestId("dags-success-filter")).toBeInTheDocument()); - await waitFor(() => screen.getByTestId("dags-success-filter").click()); + await waitFor(() => expect(screen.getByText("states.success")).toBeInTheDocument()); + await waitFor(() => screen.getByText("states.success").click()); await waitFor(() => expect(screen.getByText("tutorial_taskflow_api_success")).toBeInTheDocument()); - await waitFor(() => expect(screen.getByTestId("dags-failed-filter")).toBeInTheDocument()); - await waitFor(() => screen.getByTestId("dags-failed-filter").click()); + await waitFor(() => expect(screen.getByText("states.failed")).toBeInTheDocument()); + await waitFor(() => screen.getByText("states.failed").click()); await waitFor(() => expect(screen.getByText("tutorial_taskflow_api_failed")).toBeInTheDocument()); }); }); From 011e857899f8bf288e0dddd3cf42e25d82e545bc Mon Sep 17 00:00:00 2001 From: Brent Bovenzi Date: Sat, 10 Jan 2026 10:21:01 -0500 Subject: [PATCH 3/3] Remove manual bg setting --- .../airflow/ui/src/components/DataTable/ToggleTableDisplay.tsx | 2 -- .../src/airflow/ui/src/components/ui/ButtonGroupToggle.tsx | 1 - .../ui/src/pages/DagsList/DagsFilters/RequiredActionFilter.tsx | 1 - 3 files changed, 4 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/components/DataTable/ToggleTableDisplay.tsx b/airflow-core/src/airflow/ui/src/components/DataTable/ToggleTableDisplay.tsx index 0c0b6e4ab99d8..6b28ba0b9587c 100644 --- a/airflow-core/src/airflow/ui/src/components/DataTable/ToggleTableDisplay.tsx +++ b/airflow-core/src/airflow/ui/src/components/DataTable/ToggleTableDisplay.tsx @@ -34,7 +34,6 @@ export const ToggleTableDisplay = ({ display, setDisplay }: Props) => { setDisplay("card")} title={translate("toggleCardView")} variant={display === "card" ? "solid" : "outline"} @@ -43,7 +42,6 @@ export const ToggleTableDisplay = ({ display, setDisplay }: Props) => { setDisplay("table")} title={translate("toggleTableView")} variant={display === "table" ? "solid" : "outline"} diff --git a/airflow-core/src/airflow/ui/src/components/ui/ButtonGroupToggle.tsx b/airflow-core/src/airflow/ui/src/components/ui/ButtonGroupToggle.tsx index d5196b522b5d1..90dbdd9c3e3ff 100644 --- a/airflow-core/src/airflow/ui/src/components/ui/ButtonGroupToggle.tsx +++ b/airflow-core/src/airflow/ui/src/components/ui/ButtonGroupToggle.tsx @@ -45,7 +45,6 @@ export const ButtonGroupToggle = ({ return (