From 5968117a5b8c7e8640b452fc37400864b00420df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=96=86=E5=AE=B8?= Date: Sun, 24 Aug 2025 21:12:29 -0400 Subject: [PATCH 01/14] feat(ui): add reusable FilterBar component for date, number, text input --- .../ui/public/i18n/locales/en/common.json | 2 + .../ui/public/i18n/locales/zh-TW/common.json | 2 + .../ui/src/components/FilterBar/FilterBar.tsx | 179 +++++++++++ .../src/components/FilterBar/FilterPill.tsx | 129 ++++++++ .../src/components/FilterBar/defaultIcons.tsx | 29 ++ .../FilterBar/filters/DateFilter.tsx | 60 ++++ .../FilterBar/filters/NumberFilter.tsx | 65 ++++ .../FilterBar/filters/TextSearchFilter.tsx | 66 ++++ .../ui/src/components/FilterBar/index.ts | 25 ++ .../ui/src/components/FilterBar/types.ts | 53 +++ .../airflow/ui/src/pages/XCom/XComFilters.tsx | 301 +++++++----------- 11 files changed, 733 insertions(+), 178 deletions(-) create mode 100644 airflow-core/src/airflow/ui/src/components/FilterBar/FilterBar.tsx create mode 100644 airflow-core/src/airflow/ui/src/components/FilterBar/FilterPill.tsx create mode 100644 airflow-core/src/airflow/ui/src/components/FilterBar/defaultIcons.tsx create mode 100644 airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateFilter.tsx create mode 100644 airflow-core/src/airflow/ui/src/components/FilterBar/filters/NumberFilter.tsx create mode 100644 airflow-core/src/airflow/ui/src/components/FilterBar/filters/TextSearchFilter.tsx create mode 100644 airflow-core/src/airflow/ui/src/components/FilterBar/index.ts create mode 100644 airflow-core/src/airflow/ui/src/components/FilterBar/types.ts diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json index defe64cce63c1..9926fc8e1697c 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json @@ -53,6 +53,7 @@ "tags": "Tags" }, "dagId": "Dag ID", + "dagName": "Dag Name", "dagRun": { "conf": "Conf", "dagVersions": "Dag Version(s)", @@ -101,6 +102,7 @@ }, "filters": { "dagDisplayNamePlaceholder": "Filter by Dag", + "filter": "Filter", "keyPlaceholder": "Filter by XCom key", "logicalDateFromPlaceholder": "Logical Date From", "logicalDateToPlaceholder": "Logical Date To", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/common.json index cf89c80b5569b..434e944d31dd3 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/common.json @@ -52,6 +52,7 @@ "tags": "標籤" }, "dagId": "Dag ID", + "dagName": "Dag 名稱", "dagRun": { "conf": "設定", "dagVersions": "Dag 版本", @@ -100,6 +101,7 @@ }, "filters": { "dagDisplayNamePlaceholder": "依 Dag 名稱篩選", + "filter": "篩選", "keyPlaceholder": "依 XCom 鍵篩選", "logicalDateFromPlaceholder": "邏輯日期起始", "logicalDateToPlaceholder": "邏輯日期結束", diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/FilterBar.tsx b/airflow-core/src/airflow/ui/src/components/FilterBar/FilterBar.tsx new file mode 100644 index 0000000000000..317a389c8976e --- /dev/null +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/FilterBar.tsx @@ -0,0 +1,179 @@ +/*! + * 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, HStack } from "@chakra-ui/react"; +import { useState, useCallback } from "react"; +import { useTranslation } from "react-i18next"; +import { MdAdd, MdClear } from "react-icons/md"; +import { useDebouncedCallback } from "use-debounce"; + +import { Menu } from "src/components/ui"; + +import { getDefaultFilterIcon } from "./defaultIcons"; +import { DateFilter } from "./filters/DateFilter"; +import { NumberFilter } from "./filters/NumberFilter"; +import { TextSearchFilter } from "./filters/TextSearchFilter"; +import type { FilterBarProps, FilterConfig, FilterState, FilterValue } from "./types"; + +const defaultInitialValues: Record = {}; + +const getFilterIcon = (config: FilterConfig) => config.icon ?? getDefaultFilterIcon(config.type); + +export const FilterBar = ({ + configs, + initialValues = defaultInitialValues, + maxVisibleFilters = 10, + onFiltersChange, +}: FilterBarProps) => { + const { t: translate } = useTranslation(); + const [filters, setFilters] = useState>(() => + Object.entries(initialValues) + .filter(([, value]) => value !== null && value !== undefined && value !== "") + .map(([key, value]) => { + const config = configs.find((con) => con.key === key); + + if (!config) { + throw new Error(`Filter config not found for key: ${key}`); + } + + return { + config, + id: `${key}-${Date.now()}`, + value, + }; + }), + ); + + const debouncedOnFiltersChange = useDebouncedCallback((filtersRecord: Record) => { + onFiltersChange(filtersRecord); + }, 100); + + const updateFiltersRecord = useCallback( + (updatedFilters: Array) => { + const filtersRecord = updatedFilters.reduce>((accumulator, filter) => { + if (filter.value !== null && filter.value !== undefined && filter.value !== "") { + accumulator[filter.config.key] = filter.value; + } + + return accumulator; + }, {}); + + debouncedOnFiltersChange(filtersRecord); + }, + [debouncedOnFiltersChange], + ); + + const addFilter = (config: FilterConfig) => { + const newFilter: FilterState = { + config, + id: `${config.key}-${Date.now()}`, + value: config.defaultValue ?? "", + }; + + const updatedFilters = [...filters, newFilter]; + + setFilters(updatedFilters); + updateFiltersRecord(updatedFilters); + }; + + const updateFilter = (id: string, value: FilterValue) => { + const updatedFilters = filters.map((filter) => (filter.id === id ? { ...filter, value } : filter)); + + setFilters(updatedFilters); + updateFiltersRecord(updatedFilters); + }; + + const removeFilter = (id: string) => { + const updatedFilters = filters.filter((filter) => filter.id !== id); + + setFilters(updatedFilters); + updateFiltersRecord(updatedFilters); + }; + + const resetFilters = () => { + setFilters([]); + onFiltersChange({}); + }; + + const availableConfigs = configs.filter( + (config) => !filters.some((filter) => filter.config.key === config.key), + ); + + const renderFilter = (filter: FilterState) => { + const props = { + filter, + onChange: (value: FilterValue) => updateFilter(filter.id, value), + onRemove: () => removeFilter(filter.id), + }; + + switch (filter.config.type) { + case "date": + return ; + case "number": + return ; + case "text": + return ; + default: + return undefined; + } + }; + + return ( + + {filters.slice(0, maxVisibleFilters).map(renderFilter)} + {availableConfigs.length > 0 && ( + + + + + + {availableConfigs.map((config) => ( + addFilter(config)} value={config.key}> + + {getFilterIcon(config)} + {config.label} + + + ))} + + + )} + {filters.length > 0 && ( + + )} + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/FilterPill.tsx b/airflow-core/src/airflow/ui/src/components/FilterBar/FilterPill.tsx new file mode 100644 index 0000000000000..11e460d3f0e61 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/FilterPill.tsx @@ -0,0 +1,129 @@ +/*! + * 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 { Box, Button, HStack, IconButton } from "@chakra-ui/react"; +import React from "react"; +import { useEffect, useState } from "react"; +import { MdClose } from "react-icons/md"; + +import { getDefaultFilterIcon } from "./defaultIcons"; +import type { FilterState, FilterValue } from "./types"; + +type FilterPillProps = { + readonly children: React.ReactNode; + readonly displayValue: string; + readonly filter: FilterState; + readonly hasValue: boolean; + readonly onChange: (value: FilterValue) => void; + readonly onRemove: () => void; +}; + +export const FilterPill = ({ + children, + displayValue, + filter, + hasValue, + onChange, + onRemove, +}: FilterPillProps) => { + const isEmpty = filter.value === null || filter.value === undefined || String(filter.value).trim() === ""; + const [isEditing, setIsEditing] = useState(isEmpty); + + const handlePillClick = () => setIsEditing(true); + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === "Enter" || event.key === "Escape") { + setIsEditing(false); + } + }; + + const handleBlur = () => { + setTimeout(() => setIsEditing(false), 100); + }; + + useEffect(() => { + if (isEditing) { + setTimeout(() => { + const input = document.querySelector(`input[placeholder*="${filter.config.label}"]`); + + if (input instanceof HTMLInputElement) { + input.focus(); + } + }, 10); + } + }, [isEditing, filter.config.label]); + + const childrenWithProps = React.Children.map(children, (child) => { + if (React.isValidElement(child)) { + return React.cloneElement(child, { + autoFocus: true, + onBlur: handleBlur, + onChange, + onKeyDown: handleKeyDown, + ...child.props, + } as Record); + } + + return child; + }); + + if (isEditing) { + return childrenWithProps; + } + + return ( + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/defaultIcons.tsx b/airflow-core/src/airflow/ui/src/components/FilterBar/defaultIcons.tsx new file mode 100644 index 0000000000000..bb622ecbd919a --- /dev/null +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/defaultIcons.tsx @@ -0,0 +1,29 @@ +/*! + * 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 { MdCalendarToday, MdNumbers, MdTextFields } from "react-icons/md"; + +import type { FilterConfig } from "./types"; + +export const defaultFilterIcons = { + date: , + number: , + text: , +} as const; + +export const getDefaultFilterIcon = (type: FilterConfig["type"]) => defaultFilterIcons[type]; diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateFilter.tsx b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateFilter.tsx new file mode 100644 index 0000000000000..fad2e00c7bad9 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateFilter.tsx @@ -0,0 +1,60 @@ +/*! + * 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 { DateTimeInput } from "src/components/DateTimeInput"; + +import { FilterPill } from "../FilterPill"; +import type { FilterPluginProps } from "../types"; + +export const DateFilter = ({ filter, onChange, onRemove }: FilterPluginProps) => { + const hasValue = filter.value !== null && filter.value !== undefined && String(filter.value).trim() !== ""; + const displayValue = hasValue + ? new Date(String(filter.value)).toLocaleString("en-US", { + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + month: "short", + year: "numeric", + }) + : ""; + + const handleDateChange = (event: React.ChangeEvent) => { + const { value } = event.target; + + onChange(value || undefined); + }; + + return ( + + + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/NumberFilter.tsx b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/NumberFilter.tsx new file mode 100644 index 0000000000000..e602699cd17a7 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/NumberFilter.tsx @@ -0,0 +1,65 @@ +/*! + * 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 { NumberInputField, NumberInputRoot } from "src/components/ui/NumberInput"; + +import { FilterPill } from "../FilterPill"; +import type { FilterPluginProps } from "../types"; + +export const NumberFilter = ({ filter, onChange, onRemove }: FilterPluginProps) => { + const hasValue = filter.value !== null && filter.value !== undefined && filter.value !== ""; + + const handleValueChange = ({ value }: { value: string }) => { + if (value === "") { + onChange(undefined); + + return; + } + + const numValue = Number(value); + + if (!isNaN(numValue)) { + onChange(numValue); + } + }; + + return ( + + + + + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/TextSearchFilter.tsx b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/TextSearchFilter.tsx new file mode 100644 index 0000000000000..119d8ea1f0566 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/TextSearchFilter.tsx @@ -0,0 +1,66 @@ +/*! + * 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 { Input } from "@chakra-ui/react"; +import { useRef } from "react"; +import { useHotkeys } from "react-hotkeys-hook"; + +import { FilterPill } from "../FilterPill"; +import type { FilterPluginProps } from "../types"; + +export const TextSearchFilter = ({ filter, onChange, onRemove }: FilterPluginProps) => { + const inputRef = useRef(null); + + const hasValue = filter.value !== null && filter.value !== undefined && String(filter.value).trim() !== ""; + + const handleInputChange = (event: React.ChangeEvent) => { + const newValue = event.target.value; + + onChange(newValue || undefined); + }; + + useHotkeys( + "mod+k", + () => { + if (!filter.config.hotkeyDisabled) { + inputRef.current?.focus(); + } + }, + { enabled: !filter.config.hotkeyDisabled, preventDefault: true }, + ); + + return ( + + + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/index.ts b/airflow-core/src/airflow/ui/src/components/FilterBar/index.ts new file mode 100644 index 0000000000000..fa3c5c7621da0 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/index.ts @@ -0,0 +1,25 @@ +/*! + * 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. + */ +export { FilterBar } from "./FilterBar"; +export { FilterPill } from "./FilterPill"; +export { defaultFilterIcons, getDefaultFilterIcon } from "./defaultIcons"; +export { DateFilter } from "./filters/DateFilter"; +export { NumberFilter } from "./filters/NumberFilter"; +export { TextSearchFilter } from "./filters/TextSearchFilter"; +export type * from "./types"; diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/types.ts b/airflow-core/src/airflow/ui/src/components/FilterBar/types.ts new file mode 100644 index 0000000000000..0b3820e5c7107 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/types.ts @@ -0,0 +1,53 @@ +/*! + * 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 React from "react"; + +export type FilterValue = Date | number | string | null | undefined; + +export type FilterConfig = { + readonly defaultValue?: FilterValue; + readonly hotkeyDisabled?: boolean; + readonly icon?: React.ReactNode; + readonly key: string; + readonly label: string; + readonly max?: number; + readonly min?: number; + readonly placeholder?: string; + readonly required?: boolean; + readonly type: "date" | "number" | "text"; +}; + +export type FilterState = { + readonly config: FilterConfig; + readonly id: string; + readonly value: FilterValue; +}; + +export type FilterBarProps = { + readonly configs: Array; + readonly initialValues?: Record; + readonly maxVisibleFilters?: number; + readonly onFiltersChange: (filters: Record) => void; +}; + +export type FilterPluginProps = { + readonly filter: FilterState; + readonly onChange: (value: FilterValue) => void; + readonly onRemove: () => void; +}; diff --git a/airflow-core/src/airflow/ui/src/pages/XCom/XComFilters.tsx b/airflow-core/src/airflow/ui/src/pages/XCom/XComFilters.tsx index c7f36cfdd1c9f..fda3792d897bb 100644 --- a/airflow-core/src/airflow/ui/src/pages/XCom/XComFilters.tsx +++ b/airflow-core/src/airflow/ui/src/pages/XCom/XComFilters.tsx @@ -16,206 +16,151 @@ * specific language governing permissions and limitations * under the License. */ -import { Box, Button, HStack, Text, VStack } from "@chakra-ui/react"; -import { useCallback, useMemo, useState } from "react"; +import { VStack } from "@chakra-ui/react"; +import { useCallback, useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { LuX } from "react-icons/lu"; -import { useSearchParams, useParams } from "react-router-dom"; +import { FiBarChart } from "react-icons/fi"; +import { MdDateRange, MdNumbers, MdSearch } from "react-icons/md"; +import { useParams, useSearchParams } from "react-router-dom"; +import { DagIcon } from "src/assets/DagIcon"; +import { TaskIcon } from "src/assets/TaskIcon"; import { useTableURLState } from "src/components/DataTable/useTableUrlState"; -import { DateTimeInput } from "src/components/DateTimeInput"; -import { SearchBar } from "src/components/SearchBar"; -import { NumberInputField, NumberInputRoot } from "src/components/ui/NumberInput"; +import { FilterBar, type FilterConfig, type FilterValue } from "src/components/FilterBar"; import { SearchParamsKeys } from "src/constants/searchParams"; -const FILTERS = [ - { - hotkeyDisabled: false, - key: SearchParamsKeys.KEY_PATTERN, - translationKey: "keyPlaceholder", - type: "search", - }, - { - hotkeyDisabled: true, - key: SearchParamsKeys.DAG_DISPLAY_NAME_PATTERN, - translationKey: "dagDisplayNamePlaceholder", - type: "search", - }, - { - hotkeyDisabled: true, - key: SearchParamsKeys.RUN_ID_PATTERN, - translationKey: "runIdPlaceholder", - type: "search", - }, - { - hotkeyDisabled: true, - key: SearchParamsKeys.TASK_ID_PATTERN, - translationKey: "taskIdPlaceholder", - type: "search", - }, - { - hotkeyDisabled: true, - key: SearchParamsKeys.MAP_INDEX, - translationKey: "mapIndexPlaceholder", - type: "number", - }, - { - key: SearchParamsKeys.LOGICAL_DATE_GTE, - translationKey: "logicalDateFromPlaceholder", - type: "datetime", - }, - { - key: SearchParamsKeys.LOGICAL_DATE_LTE, - translationKey: "logicalDateToPlaceholder", - type: "datetime", - }, - { - key: SearchParamsKeys.RUN_AFTER_GTE, - translationKey: "runAfterFromPlaceholder", - type: "datetime", - }, - { - key: SearchParamsKeys.RUN_AFTER_LTE, - translationKey: "runAfterToPlaceholder", - type: "datetime", - }, -] as const satisfies ReadonlyArray<{ - readonly hotkeyDisabled?: boolean; - readonly key: string; - readonly translationKey: string; - readonly type: "datetime" | "number" | "search"; -}>; - export const XComFilters = () => { const [searchParams, setSearchParams] = useSearchParams(); const { dagId = "~", mapIndex = "-1", runId = "~", taskId = "~" } = useParams(); const { setTableURLState, tableURLState } = useTableURLState(); const { pagination, sorting } = tableURLState; - const { t: translate } = useTranslation(["browse", "common"]); - const [resetKey, setResetKey] = useState(0); - - const visibleFilters = useMemo( - () => - FILTERS.filter((filter) => { - switch (filter.key) { - case SearchParamsKeys.DAG_DISPLAY_NAME_PATTERN: - return dagId === "~"; - case SearchParamsKeys.KEY_PATTERN: - case SearchParamsKeys.LOGICAL_DATE_GTE: - case SearchParamsKeys.LOGICAL_DATE_LTE: - case SearchParamsKeys.RUN_AFTER_GTE: - case SearchParamsKeys.RUN_AFTER_LTE: - return true; - case SearchParamsKeys.MAP_INDEX: - return mapIndex === "-1"; - case SearchParamsKeys.RUN_ID_PATTERN: - return runId === "~"; - case SearchParamsKeys.TASK_ID_PATTERN: - return taskId === "~"; - default: - return true; - } - }), - [dagId, mapIndex, runId, taskId], - ); + const { t: translate } = useTranslation(["browse", "common", "admin"]); + + const filterConfigs: Array = useMemo(() => { + const configs: Array = [ + { + icon: , + key: SearchParamsKeys.KEY_PATTERN, + label: translate("admin:columns.key"), + placeholder: translate("common:filters.keyPlaceholder"), + type: "text", + }, + { + icon: , + key: SearchParamsKeys.LOGICAL_DATE_GTE, + label: translate("common:filters.logicalDateFromPlaceholder"), + placeholder: translate("common:filters.logicalDateFromPlaceholder"), + type: "date", + }, + { + icon: , + key: SearchParamsKeys.LOGICAL_DATE_LTE, + label: translate("common:filters.logicalDateToPlaceholder"), + placeholder: translate("common:filters.logicalDateToPlaceholder"), + type: "date", + }, + { + icon: , + key: SearchParamsKeys.RUN_AFTER_GTE, + label: translate("common:filters.runAfterFromPlaceholder"), + placeholder: translate("common:filters.runAfterFromPlaceholder"), + type: "date", + }, + { + icon: , + key: SearchParamsKeys.RUN_AFTER_LTE, + label: translate("common:filters.runAfterToPlaceholder"), + placeholder: translate("common:filters.runAfterToPlaceholder"), + type: "date", + }, + ]; + + if (dagId === "~") { + configs.push({ + icon: , + key: SearchParamsKeys.DAG_DISPLAY_NAME_PATTERN, + label: translate("common:dagName"), + placeholder: translate("common:filters.dagDisplayNamePlaceholder"), + type: "text", + }); + } + + if (runId === "~") { + configs.push({ + icon: , + key: SearchParamsKeys.RUN_ID_PATTERN, + label: translate("common:runId"), + placeholder: translate("common:filters.runIdPlaceholder"), + type: "text", + }); + } + + if (taskId === "~") { + configs.push({ + icon: , + key: SearchParamsKeys.TASK_ID_PATTERN, + label: translate("common:taskId"), + placeholder: translate("common:filters.taskIdPlaceholder"), + type: "text", + }); + } + + if (mapIndex === "-1") { + configs.push({ + icon: , + key: SearchParamsKeys.MAP_INDEX, + label: translate("common:mapIndex"), + min: -1, + placeholder: translate("common:filters.mapIndexPlaceholder"), + type: "number", + }); + } + + return configs; + }, [dagId, mapIndex, runId, taskId, translate]); + + const initialValues = useMemo(() => { + const values: Record = {}; - const handleFilterChange = useCallback( - (paramKey: string) => (value: string) => { - if (value === "") { - searchParams.delete(paramKey); - } else { - searchParams.set(paramKey, value); + filterConfigs.forEach((config) => { + const value = searchParams.get(config.key); + + if (value !== null && value !== "") { + values[config.key] = config.type === "number" ? Number(value) : value; } + }); + + return values; + }, [searchParams, filterConfigs]); + + const handleFiltersChange = useCallback( + (filters: Record) => { + filterConfigs.forEach((config) => { + const value = filters[config.key]; + + if (value === null || value === undefined || value === "") { + searchParams.delete(config.key); + } else { + searchParams.set(config.key, String(value)); + } + }); + setTableURLState({ pagination: { ...pagination, pageIndex: 0 }, sorting, }); setSearchParams(searchParams); }, - [pagination, searchParams, setSearchParams, setTableURLState, sorting], - ); - - const filterCount = useMemo( - () => - visibleFilters.filter((filter) => { - const value = searchParams.get(filter.key); - - return value !== null && value !== ""; - }).length, - [searchParams, visibleFilters], + [filterConfigs, pagination, searchParams, setSearchParams, setTableURLState, sorting], ); - const handleResetFilters = useCallback(() => { - visibleFilters.forEach((filter) => { - searchParams.delete(filter.key); - }); - setTableURLState({ - pagination: { ...pagination, pageIndex: 0 }, - sorting, - }); - setSearchParams(searchParams); - setResetKey((prev) => prev + 1); - }, [pagination, searchParams, setSearchParams, setTableURLState, sorting, visibleFilters]); - - const renderFilterInput = (filter: (typeof FILTERS)[number]) => { - const { key, translationKey, type } = filter; - - return ( - - - {type !== "search" && {translate(`common:filters.${translationKey}`)}} - - {type === "search" ? ( - (() => { - const { hotkeyDisabled } = filter; - - return ( - - ); - })() - ) : type === "datetime" ? ( - handleFilterChange(key)(event.target.value)} - value={searchParams.get(key) ?? ""} - /> - ) : ( - handleFilterChange(key)(details.value)} - value={searchParams.get(key) ?? ""} - > - - - )} - - ); - }; - return ( - - {visibleFilters.map(renderFilterInput)} - - -   - - {filterCount > 0 && ( - - )} - - + ); }; From 8fca5e1d61e4dc9971ec8ed7af0ced6bf7a1458c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=96=86=E5=AE=B8?= Date: Mon, 25 Aug 2025 00:07:04 -0400 Subject: [PATCH 02/14] fix(ui): improve FilterPill focus and blur handling --- .../src/components/FilterBar/FilterPill.tsx | 41 ++++++++++++++----- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/FilterPill.tsx b/airflow-core/src/airflow/ui/src/components/FilterBar/FilterPill.tsx index 11e460d3f0e61..24034ec43ced5 100644 --- a/airflow-core/src/airflow/ui/src/components/FilterBar/FilterPill.tsx +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/FilterPill.tsx @@ -18,7 +18,7 @@ */ import { Box, Button, HStack, IconButton } from "@chakra-ui/react"; import React from "react"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { MdClose } from "react-icons/md"; import { getDefaultFilterIcon } from "./defaultIcons"; @@ -43,6 +43,8 @@ export const FilterPill = ({ }: FilterPillProps) => { const isEmpty = filter.value === null || filter.value === undefined || String(filter.value).trim() === ""; const [isEditing, setIsEditing] = useState(isEmpty); + const inputRef = useRef(null); + const blurTimeoutRef = useRef(undefined); const handlePillClick = () => setIsEditing(true); @@ -53,28 +55,45 @@ export const FilterPill = ({ }; const handleBlur = () => { - setTimeout(() => setIsEditing(false), 100); + blurTimeoutRef.current = setTimeout(() => setIsEditing(false), 150); + }; + + const handleFocus = () => { + if (blurTimeoutRef.current) { + clearTimeout(blurTimeoutRef.current); + blurTimeoutRef.current = undefined; + } }; useEffect(() => { - if (isEditing) { - setTimeout(() => { - const input = document.querySelector(`input[placeholder*="${filter.config.label}"]`); + if (isEditing && inputRef.current) { + const input = inputRef.current; + const focusInput = () => { + input.focus(); + input.select(); + }; - if (input instanceof HTMLInputElement) { - input.focus(); - } - }, 10); + requestAnimationFrame(focusInput); } - }, [isEditing, filter.config.label]); + }, [isEditing]); + + useEffect( + () => () => { + if (blurTimeoutRef.current) { + clearTimeout(blurTimeoutRef.current); + } + }, + [], + ); const childrenWithProps = React.Children.map(children, (child) => { if (React.isValidElement(child)) { return React.cloneElement(child, { - autoFocus: true, onBlur: handleBlur, onChange, + onFocus: handleFocus, onKeyDown: handleKeyDown, + ref: inputRef, ...child.props, } as Record); } From fc96c9e3a446585ba104ba6537ae8911f51548a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=96=86=E5=AE=B8?= Date: Mon, 25 Aug 2025 18:48:57 -0400 Subject: [PATCH 03/14] refactor(ui): create useFiltersHandler hook for FilterBar --- .../airflow/ui/src/pages/XCom/XComFilters.tsx | 32 ++--------- .../src/airflow/ui/src/utils/index.ts | 1 + .../airflow/ui/src/utils/useFiltersHandler.ts | 55 +++++++++++++++++++ 3 files changed, 61 insertions(+), 27 deletions(-) create mode 100644 airflow-core/src/airflow/ui/src/utils/useFiltersHandler.ts diff --git a/airflow-core/src/airflow/ui/src/pages/XCom/XComFilters.tsx b/airflow-core/src/airflow/ui/src/pages/XCom/XComFilters.tsx index fda3792d897bb..9d9fd58b8e35b 100644 --- a/airflow-core/src/airflow/ui/src/pages/XCom/XComFilters.tsx +++ b/airflow-core/src/airflow/ui/src/pages/XCom/XComFilters.tsx @@ -17,23 +17,20 @@ * under the License. */ import { VStack } from "@chakra-ui/react"; -import { useCallback, useMemo } from "react"; +import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import { FiBarChart } from "react-icons/fi"; import { MdDateRange, MdNumbers, MdSearch } from "react-icons/md"; -import { useParams, useSearchParams } from "react-router-dom"; +import { useParams } from "react-router-dom"; import { DagIcon } from "src/assets/DagIcon"; import { TaskIcon } from "src/assets/TaskIcon"; -import { useTableURLState } from "src/components/DataTable/useTableUrlState"; import { FilterBar, type FilterConfig, type FilterValue } from "src/components/FilterBar"; import { SearchParamsKeys } from "src/constants/searchParams"; +import { useFiltersHandler } from "src/utils"; export const XComFilters = () => { - const [searchParams, setSearchParams] = useSearchParams(); const { dagId = "~", mapIndex = "-1", runId = "~", taskId = "~" } = useParams(); - const { setTableURLState, tableURLState } = useTableURLState(); - const { pagination, sorting } = tableURLState; const { t: translate } = useTranslation(["browse", "common", "admin"]); const filterConfigs: Array = useMemo(() => { @@ -119,6 +116,8 @@ export const XComFilters = () => { return configs; }, [dagId, mapIndex, runId, taskId, translate]); + const { handleFiltersChange, searchParams } = useFiltersHandler(filterConfigs); + const initialValues = useMemo(() => { const values: Record = {}; @@ -133,27 +132,6 @@ export const XComFilters = () => { return values; }, [searchParams, filterConfigs]); - const handleFiltersChange = useCallback( - (filters: Record) => { - filterConfigs.forEach((config) => { - const value = filters[config.key]; - - if (value === null || value === undefined || value === "") { - searchParams.delete(config.key); - } else { - searchParams.set(config.key, String(value)); - } - }); - - setTableURLState({ - pagination: { ...pagination, pageIndex: 0 }, - sorting, - }); - setSearchParams(searchParams); - }, - [filterConfigs, pagination, searchParams, setSearchParams, setTableURLState, sorting], - ); - return ( ) => { + const [searchParams, setSearchParams] = useSearchParams(); + const { setTableURLState, tableURLState } = useTableURLState(); + const { pagination, sorting } = tableURLState; + + const handleFiltersChange = useCallback( + (filters: Record) => { + filterConfigs.forEach((config) => { + const value = filters[config.key]; + + if (value === null || value === undefined || value === "") { + searchParams.delete(config.key); + } else { + searchParams.set(config.key, String(value)); + } + }); + + setTableURLState({ + pagination: { ...pagination, pageIndex: 0 }, + sorting, + }); + setSearchParams(searchParams); + }, + [filterConfigs, pagination, searchParams, setSearchParams, setTableURLState, sorting], + ); + + return { + handleFiltersChange, + searchParams, + }; +}; From ad90651a7287716bd6aae20ae80bfafc4d84ee10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=96=86=E5=AE=B8?= Date: Mon, 25 Aug 2025 23:16:26 -0400 Subject: [PATCH 04/14] refactor(ui):Support negative number input & fix FilterPill DOM nesting --- .../src/components/FilterBar/FilterPill.tsx | 37 +++++++++++++------ .../FilterBar/filters/NumberFilter.tsx | 25 ++++++++++--- .../airflow/ui/src/pages/XCom/XComFilters.tsx | 8 +++- 3 files changed, 52 insertions(+), 18 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/FilterPill.tsx b/airflow-core/src/airflow/ui/src/components/FilterBar/FilterPill.tsx index 24034ec43ced5..f8ce033a3e07f 100644 --- a/airflow-core/src/airflow/ui/src/components/FilterBar/FilterPill.tsx +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/FilterPill.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { Box, Button, HStack, IconButton } from "@chakra-ui/react"; +import { Box, HStack } from "@chakra-ui/react"; import React from "react"; import { useEffect, useRef, useState } from "react"; import { MdClose } from "react-icons/md"; @@ -70,7 +70,11 @@ export const FilterPill = ({ const input = inputRef.current; const focusInput = () => { input.focus(); - input.select(); + try { + input.select(); + } catch { + // NumberInputField doesn't support select() + } }; requestAnimationFrame(focusInput); @@ -106,43 +110,52 @@ export const FilterPill = ({ } return ( - + ); }; diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/NumberFilter.tsx b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/NumberFilter.tsx index e602699cd17a7..e2b99c2583364 100644 --- a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/NumberFilter.tsx +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/NumberFilter.tsx @@ -16,6 +16,8 @@ * specific language governing permissions and limitations * under the License. */ +import { useState, useEffect } from "react"; + import { NumberInputField, NumberInputRoot } from "src/components/ui/NumberInput"; import { FilterPill } from "../FilterPill"; @@ -24,17 +26,30 @@ import type { FilterPluginProps } from "../types"; export const NumberFilter = ({ filter, onChange, onRemove }: FilterPluginProps) => { const hasValue = filter.value !== null && filter.value !== undefined && filter.value !== ""; + const [inputValue, setInputValue] = useState(filter.value?.toString() ?? ""); + + useEffect(() => { + setInputValue(filter.value?.toString() ?? ""); + }, [filter.value]); + const handleValueChange = ({ value }: { value: string }) => { + setInputValue(value); + if (value === "") { onChange(undefined); return; } - const numValue = Number(value); + // Allow user to input negative sign for negative number + if (value === "-") { + return; + } + + const parsedValue = Number(value); - if (!isNaN(numValue)) { - onChange(numValue); + if (!isNaN(parsedValue)) { + onChange(parsedValue); } }; @@ -49,10 +64,10 @@ export const NumberFilter = ({ filter, onChange, onRemove }: FilterPluginProps) { const value = searchParams.get(config.key); if (value !== null && value !== "") { - values[config.key] = config.type === "number" ? Number(value) : value; + if (config.type === "number") { + const parsedValue = Number(value); + + values[config.key] = isNaN(parsedValue) ? value : parsedValue; + } else { + values[config.key] = value; + } } }); From b87f7834498c954c8575f1ba0bedbe7eca66582d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=96=86=E5=AE=B8?= Date: Tue, 26 Aug 2025 00:49:25 -0400 Subject: [PATCH 05/14] feat(ui): create centralized filter config --- .../ui/src/constants/filterConfigs.tsx | 110 ++++++++++++++++++ .../airflow/ui/src/pages/XCom/XComFilters.tsx | 83 ++----------- 2 files changed, 122 insertions(+), 71 deletions(-) create mode 100644 airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx diff --git a/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx b/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx new file mode 100644 index 0000000000000..f4c054931d5ce --- /dev/null +++ b/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx @@ -0,0 +1,110 @@ +/*! + * 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 { useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { FiBarChart } from "react-icons/fi"; +import { MdDateRange, MdNumbers, MdSearch } from "react-icons/md"; + +import { DagIcon } from "src/assets/DagIcon"; +import { TaskIcon } from "src/assets/TaskIcon"; +import type { FilterConfig } from "src/components/FilterBar"; + +import { SearchParamsKeys } from "./searchParams"; + +/** + * Hook to get filter configurations with translations + */ +export const useFilterConfigs = () => { + const { t: translate } = useTranslation(["browse", "common", "admin"]); + + const filterConfigMap = useMemo( + () => ({ + [SearchParamsKeys.DAG_DISPLAY_NAME_PATTERN]: { + hotkeyDisabled: true, + icon: , + label: translate("common:dagName"), + placeholder: translate("common:filters.dagDisplayNamePlaceholder"), + type: "text" as const, + }, + [SearchParamsKeys.KEY_PATTERN]: { + icon: , + label: translate("admin:columns.key"), + placeholder: translate("common:filters.keyPlaceholder"), + type: "text" as const, + }, + [SearchParamsKeys.LOGICAL_DATE_GTE]: { + icon: , + label: translate("common:filters.logicalDateFromPlaceholder"), + placeholder: translate("common:filters.logicalDateFromPlaceholder"), + type: "date" as const, + }, + [SearchParamsKeys.LOGICAL_DATE_LTE]: { + icon: , + label: translate("common:filters.logicalDateToPlaceholder"), + placeholder: translate("common:filters.logicalDateToPlaceholder"), + type: "date" as const, + }, + [SearchParamsKeys.MAP_INDEX]: { + icon: , + label: translate("common:mapIndex"), + min: -1, + placeholder: translate("common:filters.mapIndexPlaceholder"), + type: "number" as const, + }, + [SearchParamsKeys.RUN_AFTER_GTE]: { + icon: , + label: translate("common:filters.runAfterFromPlaceholder"), + placeholder: translate("common:filters.runAfterFromPlaceholder"), + type: "date" as const, + }, + [SearchParamsKeys.RUN_AFTER_LTE]: { + icon: , + label: translate("common:filters.runAfterToPlaceholder"), + placeholder: translate("common:filters.runAfterToPlaceholder"), + type: "date" as const, + }, + [SearchParamsKeys.RUN_ID_PATTERN]: { + hotkeyDisabled: true, + icon: , + label: translate("common:runId"), + placeholder: translate("common:filters.runIdPlaceholder"), + type: "text" as const, + }, + [SearchParamsKeys.TASK_ID_PATTERN]: { + hotkeyDisabled: true, + icon: , + label: translate("common:taskId"), + placeholder: translate("common:filters.taskIdPlaceholder"), + type: "text" as const, + }, + }), + [translate], + ); + + const getFilterConfig = useMemo( + () => + (key: keyof typeof filterConfigMap): FilterConfig => ({ + key, + ...filterConfigMap[key], + }), + [filterConfigMap], + ); + + return { getFilterConfig }; +}; diff --git a/airflow-core/src/airflow/ui/src/pages/XCom/XComFilters.tsx b/airflow-core/src/airflow/ui/src/pages/XCom/XComFilters.tsx index b1a186dbafb8c..a3130e5810fd3 100644 --- a/airflow-core/src/airflow/ui/src/pages/XCom/XComFilters.tsx +++ b/airflow-core/src/airflow/ui/src/pages/XCom/XComFilters.tsx @@ -18,103 +18,44 @@ */ import { VStack } from "@chakra-ui/react"; import { useMemo } from "react"; -import { useTranslation } from "react-i18next"; -import { FiBarChart } from "react-icons/fi"; -import { MdDateRange, MdNumbers, MdSearch } from "react-icons/md"; import { useParams } from "react-router-dom"; -import { DagIcon } from "src/assets/DagIcon"; -import { TaskIcon } from "src/assets/TaskIcon"; import { FilterBar, type FilterConfig, type FilterValue } from "src/components/FilterBar"; +import { useFilterConfigs } from "src/constants/filterConfigs"; import { SearchParamsKeys } from "src/constants/searchParams"; import { useFiltersHandler } from "src/utils"; export const XComFilters = () => { const { dagId = "~", mapIndex = "-1", runId = "~", taskId = "~" } = useParams(); - const { t: translate } = useTranslation(["browse", "common", "admin"]); + const { getFilterConfig } = useFilterConfigs(); const filterConfigs: Array = useMemo(() => { const configs: Array = [ - { - icon: , - key: SearchParamsKeys.KEY_PATTERN, - label: translate("admin:columns.key"), - placeholder: translate("common:filters.keyPlaceholder"), - type: "text", - }, - { - icon: , - key: SearchParamsKeys.LOGICAL_DATE_GTE, - label: translate("common:filters.logicalDateFromPlaceholder"), - placeholder: translate("common:filters.logicalDateFromPlaceholder"), - type: "date", - }, - { - icon: , - key: SearchParamsKeys.LOGICAL_DATE_LTE, - label: translate("common:filters.logicalDateToPlaceholder"), - placeholder: translate("common:filters.logicalDateToPlaceholder"), - type: "date", - }, - { - icon: , - key: SearchParamsKeys.RUN_AFTER_GTE, - label: translate("common:filters.runAfterFromPlaceholder"), - placeholder: translate("common:filters.runAfterFromPlaceholder"), - type: "date", - }, - { - icon: , - key: SearchParamsKeys.RUN_AFTER_LTE, - label: translate("common:filters.runAfterToPlaceholder"), - placeholder: translate("common:filters.runAfterToPlaceholder"), - type: "date", - }, + getFilterConfig(SearchParamsKeys.KEY_PATTERN), + getFilterConfig(SearchParamsKeys.LOGICAL_DATE_GTE), + getFilterConfig(SearchParamsKeys.LOGICAL_DATE_LTE), + getFilterConfig(SearchParamsKeys.RUN_AFTER_GTE), + getFilterConfig(SearchParamsKeys.RUN_AFTER_LTE), ]; if (dagId === "~") { - configs.push({ - icon: , - key: SearchParamsKeys.DAG_DISPLAY_NAME_PATTERN, - label: translate("common:dagName"), - placeholder: translate("common:filters.dagDisplayNamePlaceholder"), - type: "text", - }); + configs.push(getFilterConfig(SearchParamsKeys.DAG_DISPLAY_NAME_PATTERN)); } if (runId === "~") { - configs.push({ - icon: , - key: SearchParamsKeys.RUN_ID_PATTERN, - label: translate("common:runId"), - placeholder: translate("common:filters.runIdPlaceholder"), - type: "text", - }); + configs.push(getFilterConfig(SearchParamsKeys.RUN_ID_PATTERN)); } if (taskId === "~") { - configs.push({ - icon: , - key: SearchParamsKeys.TASK_ID_PATTERN, - label: translate("common:taskId"), - placeholder: translate("common:filters.taskIdPlaceholder"), - type: "text", - }); + configs.push(getFilterConfig(SearchParamsKeys.TASK_ID_PATTERN)); } if (mapIndex === "-1") { - configs.push({ - icon: , - key: SearchParamsKeys.MAP_INDEX, - label: translate("common:mapIndex"), - min: -1, - placeholder: translate("common:filters.mapIndexPlaceholder"), - type: "number", - }); + configs.push(getFilterConfig(SearchParamsKeys.MAP_INDEX)); } return configs; - }, [dagId, mapIndex, runId, taskId, translate]); + }, [dagId, mapIndex, runId, taskId, getFilterConfig]); const { handleFiltersChange, searchParams } = useFiltersHandler(filterConfigs); From 4da5b6ebe42824addd5e69b37743706df92168d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=96=86=E5=AE=B8?= Date: Tue, 2 Sep 2025 00:23:48 -0400 Subject: [PATCH 06/14] fix(ui): apply style feedback to FilterBar --- .../ui/src/components/FilterBar/FilterBar.tsx | 9 +-- .../FilterBar/filters/NumberFilter.tsx | 54 ++++++++++++--- .../FilterBar/filters/TextSearchFilter.tsx | 9 +-- .../ui/src/components/ui/InputWithAddon.tsx | 68 +++++++++++++++++++ .../src/airflow/ui/src/components/ui/index.ts | 1 + .../ui/src/constants/filterConfigs.tsx | 5 +- 6 files changed, 119 insertions(+), 27 deletions(-) create mode 100644 airflow-core/src/airflow/ui/src/components/ui/InputWithAddon.tsx diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/FilterBar.tsx b/airflow-core/src/airflow/ui/src/components/FilterBar/FilterBar.tsx index 317a389c8976e..e98c5ce780402 100644 --- a/airflow-core/src/airflow/ui/src/components/FilterBar/FilterBar.tsx +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/FilterBar.tsx @@ -162,14 +162,7 @@ export const FilterBar = ({ )} {filters.length > 0 && ( - diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/NumberFilter.tsx b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/NumberFilter.tsx index e2b99c2583364..02cb155647b2c 100644 --- a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/NumberFilter.tsx +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/NumberFilter.tsx @@ -16,13 +16,52 @@ * specific language governing permissions and limitations * under the License. */ -import { useState, useEffect } from "react"; +import { useState, useEffect, forwardRef } from "react"; import { NumberInputField, NumberInputRoot } from "src/components/ui/NumberInput"; import { FilterPill } from "../FilterPill"; import type { FilterPluginProps } from "../types"; +const NumberInputWithRef = forwardRef< + HTMLInputElement, + { + readonly max?: number; + readonly min?: number; + readonly onBlur?: () => void; + readonly onFocus?: () => void; + readonly onKeyDown?: (event: React.KeyboardEvent) => void; + readonly onValueChange: (details: { value: string }) => void; + readonly placeholder?: string; + readonly value: string; + } +>((props, ref) => { + const { max, min, onBlur, onFocus, onKeyDown, onValueChange, placeholder, value } = props; + + return ( + + + + ); +}); + +NumberInputWithRef.displayName = "NumberInputWithRef"; + export const NumberFilter = ({ filter, onChange, onRemove }: FilterPluginProps) => { const hasValue = filter.value !== null && filter.value !== undefined && filter.value !== ""; @@ -61,20 +100,13 @@ export const NumberFilter = ({ filter, onChange, onRemove }: FilterPluginProps) onChange={onChange} onRemove={onRemove} > - - - + /> ); }; diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/TextSearchFilter.tsx b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/TextSearchFilter.tsx index 119d8ea1f0566..a1a559fb9730a 100644 --- a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/TextSearchFilter.tsx +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/TextSearchFilter.tsx @@ -16,10 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -import { Input } from "@chakra-ui/react"; import { useRef } from "react"; import { useHotkeys } from "react-hotkeys-hook"; +import { InputWithAddon } from "../../ui"; import { FilterPill } from "../FilterPill"; import type { FilterPluginProps } from "../types"; @@ -52,14 +52,11 @@ export const TextSearchFilter = ({ filter, onChange, onRemove }: FilterPluginPro onChange={onChange} onRemove={onRemove} > - ); diff --git a/airflow-core/src/airflow/ui/src/components/ui/InputWithAddon.tsx b/airflow-core/src/airflow/ui/src/components/ui/InputWithAddon.tsx new file mode 100644 index 0000000000000..b5a49e6cdf82d --- /dev/null +++ b/airflow-core/src/airflow/ui/src/components/ui/InputWithAddon.tsx @@ -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 { InputProps } from "@chakra-ui/react"; +import { Box, Input, Text } from "@chakra-ui/react"; +import * as React from "react"; + +export type InputWithAddonProps = { + readonly label: string; + readonly width?: string; +} & InputProps; + +export const InputWithAddon = React.forwardRef((props, ref) => { + const { label, width = "220px", ...inputProps } = props; + + return ( + + + {label}: + + + + ); +}); + +InputWithAddon.displayName = "InputWithAddon"; 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 b7695029c4ea8..e39d15dd09d49 100644 --- a/airflow-core/src/airflow/ui/src/components/ui/index.ts +++ b/airflow-core/src/airflow/ui/src/components/ui/index.ts @@ -35,3 +35,4 @@ export * from "./Clipboard"; export * from "./Popover"; export * from "./Checkbox"; export * from "./ResetButton"; +export * from "./InputWithAddon"; diff --git a/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx b/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx index f4c054931d5ce..d76a3ca880e9b 100644 --- a/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx +++ b/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx @@ -19,7 +19,8 @@ import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import { FiBarChart } from "react-icons/fi"; -import { MdDateRange, MdNumbers, MdSearch } from "react-icons/md"; +import { LuBrackets } from "react-icons/lu"; +import { MdDateRange, MdSearch } from "react-icons/md"; import { DagIcon } from "src/assets/DagIcon"; import { TaskIcon } from "src/assets/TaskIcon"; @@ -61,7 +62,7 @@ export const useFilterConfigs = () => { type: "date" as const, }, [SearchParamsKeys.MAP_INDEX]: { - icon: , + icon: , label: translate("common:mapIndex"), min: -1, placeholder: translate("common:filters.mapIndexPlaceholder"), From 1cb09e489a1eab941daee6c9763a1997c015b0aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=96=86=E5=AE=B8?= Date: Tue, 2 Sep 2025 01:33:28 -0400 Subject: [PATCH 07/14] refactor(ui): move getFilterConfig calls into useFiltersHandler & remove placeholder translation fallback --- .../FilterBar/filters/DateFilter.tsx | 2 +- .../FilterBar/filters/NumberFilter.tsx | 2 +- .../FilterBar/filters/TextSearchFilter.tsx | 2 +- .../ui/src/constants/filterConfigs.tsx | 139 ++++++++---------- .../airflow/ui/src/pages/XCom/XComFilters.tsx | 34 ++--- .../src/airflow/ui/src/utils/index.ts | 2 +- .../airflow/ui/src/utils/useFiltersHandler.ts | 26 +++- 7 files changed, 107 insertions(+), 100 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateFilter.tsx b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateFilter.tsx index fad2e00c7bad9..cd0d0f1a34541 100644 --- a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateFilter.tsx +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateFilter.tsx @@ -50,7 +50,7 @@ export const DateFilter = ({ filter, onChange, onRemove }: FilterPluginProps) => diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/TextSearchFilter.tsx b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/TextSearchFilter.tsx index a1a559fb9730a..8cca8cf08532e 100644 --- a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/TextSearchFilter.tsx +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/TextSearchFilter.tsx @@ -55,7 +55,7 @@ export const TextSearchFilter = ({ filter, onChange, onRemove }: FilterPluginPro diff --git a/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx b/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx index d76a3ca880e9b..bbceed5fa539c 100644 --- a/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx +++ b/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx @@ -16,7 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import { FiBarChart } from "react-icons/fi"; import { LuBrackets } from "react-icons/lu"; @@ -28,84 +27,74 @@ import type { FilterConfig } from "src/components/FilterBar"; import { SearchParamsKeys } from "./searchParams"; -/** - * Hook to get filter configurations with translations - */ export const useFilterConfigs = () => { const { t: translate } = useTranslation(["browse", "common", "admin"]); - const filterConfigMap = useMemo( - () => ({ - [SearchParamsKeys.DAG_DISPLAY_NAME_PATTERN]: { - hotkeyDisabled: true, - icon: , - label: translate("common:dagName"), - placeholder: translate("common:filters.dagDisplayNamePlaceholder"), - type: "text" as const, - }, - [SearchParamsKeys.KEY_PATTERN]: { - icon: , - label: translate("admin:columns.key"), - placeholder: translate("common:filters.keyPlaceholder"), - type: "text" as const, - }, - [SearchParamsKeys.LOGICAL_DATE_GTE]: { - icon: , - label: translate("common:filters.logicalDateFromPlaceholder"), - placeholder: translate("common:filters.logicalDateFromPlaceholder"), - type: "date" as const, - }, - [SearchParamsKeys.LOGICAL_DATE_LTE]: { - icon: , - label: translate("common:filters.logicalDateToPlaceholder"), - placeholder: translate("common:filters.logicalDateToPlaceholder"), - type: "date" as const, - }, - [SearchParamsKeys.MAP_INDEX]: { - icon: , - label: translate("common:mapIndex"), - min: -1, - placeholder: translate("common:filters.mapIndexPlaceholder"), - type: "number" as const, - }, - [SearchParamsKeys.RUN_AFTER_GTE]: { - icon: , - label: translate("common:filters.runAfterFromPlaceholder"), - placeholder: translate("common:filters.runAfterFromPlaceholder"), - type: "date" as const, - }, - [SearchParamsKeys.RUN_AFTER_LTE]: { - icon: , - label: translate("common:filters.runAfterToPlaceholder"), - placeholder: translate("common:filters.runAfterToPlaceholder"), - type: "date" as const, - }, - [SearchParamsKeys.RUN_ID_PATTERN]: { - hotkeyDisabled: true, - icon: , - label: translate("common:runId"), - placeholder: translate("common:filters.runIdPlaceholder"), - type: "text" as const, - }, - [SearchParamsKeys.TASK_ID_PATTERN]: { - hotkeyDisabled: true, - icon: , - label: translate("common:taskId"), - placeholder: translate("common:filters.taskIdPlaceholder"), - type: "text" as const, - }, - }), - [translate], - ); + const filterConfigMap = { + [SearchParamsKeys.DAG_DISPLAY_NAME_PATTERN]: { + hotkeyDisabled: true, + icon: , + label: translate("common:dagName"), + placeholder: translate("common:filters.dagDisplayNamePlaceholder"), + type: "text" as const, + }, + [SearchParamsKeys.KEY_PATTERN]: { + icon: , + label: translate("admin:columns.key"), + placeholder: translate("common:filters.keyPlaceholder"), + type: "text" as const, + }, + [SearchParamsKeys.LOGICAL_DATE_GTE]: { + icon: , + label: translate("common:filters.logicalDateFromPlaceholder"), + placeholder: translate("common:filters.logicalDateFromPlaceholder"), + type: "date" as const, + }, + [SearchParamsKeys.LOGICAL_DATE_LTE]: { + icon: , + label: translate("common:filters.logicalDateToPlaceholder"), + placeholder: translate("common:filters.logicalDateToPlaceholder"), + type: "date" as const, + }, + [SearchParamsKeys.MAP_INDEX]: { + icon: , + label: translate("common:mapIndex"), + min: -1, + placeholder: translate("common:filters.mapIndexPlaceholder"), + type: "number" as const, + }, + [SearchParamsKeys.RUN_AFTER_GTE]: { + icon: , + label: translate("common:filters.runAfterFromPlaceholder"), + placeholder: translate("common:filters.runAfterFromPlaceholder"), + type: "date" as const, + }, + [SearchParamsKeys.RUN_AFTER_LTE]: { + icon: , + label: translate("common:filters.runAfterToPlaceholder"), + placeholder: translate("common:filters.runAfterToPlaceholder"), + type: "date" as const, + }, + [SearchParamsKeys.RUN_ID_PATTERN]: { + hotkeyDisabled: true, + icon: , + label: translate("common:runId"), + placeholder: translate("common:filters.runIdPlaceholder"), + type: "text" as const, + }, + [SearchParamsKeys.TASK_ID_PATTERN]: { + hotkeyDisabled: true, + icon: , + label: translate("common:taskId"), + placeholder: translate("common:filters.taskIdPlaceholder"), + type: "text" as const, + }, + }; - const getFilterConfig = useMemo( - () => - (key: keyof typeof filterConfigMap): FilterConfig => ({ - key, - ...filterConfigMap[key], - }), - [filterConfigMap], - ); + const getFilterConfig = (key: keyof typeof filterConfigMap): FilterConfig => ({ + key, + ...filterConfigMap[key], + }); return { getFilterConfig }; }; diff --git a/airflow-core/src/airflow/ui/src/pages/XCom/XComFilters.tsx b/airflow-core/src/airflow/ui/src/pages/XCom/XComFilters.tsx index a3130e5810fd3..4b7fec38b288b 100644 --- a/airflow-core/src/airflow/ui/src/pages/XCom/XComFilters.tsx +++ b/airflow-core/src/airflow/ui/src/pages/XCom/XComFilters.tsx @@ -20,44 +20,42 @@ import { VStack } from "@chakra-ui/react"; import { useMemo } from "react"; import { useParams } from "react-router-dom"; -import { FilterBar, type FilterConfig, type FilterValue } from "src/components/FilterBar"; -import { useFilterConfigs } from "src/constants/filterConfigs"; +import { FilterBar, type FilterValue } from "src/components/FilterBar"; import { SearchParamsKeys } from "src/constants/searchParams"; -import { useFiltersHandler } from "src/utils"; +import { useFiltersHandler, type FilterableSearchParamsKeys } from "src/utils"; export const XComFilters = () => { const { dagId = "~", mapIndex = "-1", runId = "~", taskId = "~" } = useParams(); - const { getFilterConfig } = useFilterConfigs(); - const filterConfigs: Array = useMemo(() => { - const configs: Array = [ - getFilterConfig(SearchParamsKeys.KEY_PATTERN), - getFilterConfig(SearchParamsKeys.LOGICAL_DATE_GTE), - getFilterConfig(SearchParamsKeys.LOGICAL_DATE_LTE), - getFilterConfig(SearchParamsKeys.RUN_AFTER_GTE), - getFilterConfig(SearchParamsKeys.RUN_AFTER_LTE), + const searchParamKeys = useMemo((): Array => { + const keys: Array = [ + SearchParamsKeys.KEY_PATTERN, + SearchParamsKeys.LOGICAL_DATE_GTE, + SearchParamsKeys.LOGICAL_DATE_LTE, + SearchParamsKeys.RUN_AFTER_GTE, + SearchParamsKeys.RUN_AFTER_LTE, ]; if (dagId === "~") { - configs.push(getFilterConfig(SearchParamsKeys.DAG_DISPLAY_NAME_PATTERN)); + keys.push(SearchParamsKeys.DAG_DISPLAY_NAME_PATTERN); } if (runId === "~") { - configs.push(getFilterConfig(SearchParamsKeys.RUN_ID_PATTERN)); + keys.push(SearchParamsKeys.RUN_ID_PATTERN); } if (taskId === "~") { - configs.push(getFilterConfig(SearchParamsKeys.TASK_ID_PATTERN)); + keys.push(SearchParamsKeys.TASK_ID_PATTERN); } if (mapIndex === "-1") { - configs.push(getFilterConfig(SearchParamsKeys.MAP_INDEX)); + keys.push(SearchParamsKeys.MAP_INDEX); } - return configs; - }, [dagId, mapIndex, runId, taskId, getFilterConfig]); + return keys; + }, [dagId, mapIndex, runId, taskId]); - const { handleFiltersChange, searchParams } = useFiltersHandler(filterConfigs); + const { filterConfigs, handleFiltersChange, searchParams } = useFiltersHandler(searchParamKeys); const initialValues = useMemo(() => { const values: Record = {}; diff --git a/airflow-core/src/airflow/ui/src/utils/index.ts b/airflow-core/src/airflow/ui/src/utils/index.ts index d52287ce1da02..c8d15c7cdb597 100644 --- a/airflow-core/src/airflow/ui/src/utils/index.ts +++ b/airflow-core/src/airflow/ui/src/utils/index.ts @@ -21,5 +21,5 @@ export { capitalize } from "./capitalize"; export { getDuration, renderDuration } from "./datetimeUtils"; export { getMetaKey } from "./getMetaKey"; export { useContainerWidth } from "./useContainerWidth"; -export { useFiltersHandler } from "./useFiltersHandler"; +export { useFiltersHandler, type FilterableSearchParamsKeys } from "./useFiltersHandler"; export * from "./query"; diff --git a/airflow-core/src/airflow/ui/src/utils/useFiltersHandler.ts b/airflow-core/src/airflow/ui/src/utils/useFiltersHandler.ts index 8fca5aacec220..9f4a595d85a9a 100644 --- a/airflow-core/src/airflow/ui/src/utils/useFiltersHandler.ts +++ b/airflow-core/src/airflow/ui/src/utils/useFiltersHandler.ts @@ -16,13 +16,32 @@ * specific language governing permissions and limitations * under the License. */ -import { useCallback } from "react"; +import { useCallback, useMemo } from "react"; import { useSearchParams } from "react-router-dom"; import { useTableURLState } from "src/components/DataTable/useTableUrlState"; -import type { FilterConfig, FilterValue } from "src/components/FilterBar"; +import type { FilterValue } from "src/components/FilterBar"; +import { useFilterConfigs } from "src/constants/filterConfigs"; +import type { SearchParamsKeys } from "src/constants/searchParams"; -export const useFiltersHandler = (filterConfigs: Array) => { +export type FilterableSearchParamsKeys = + | SearchParamsKeys.DAG_DISPLAY_NAME_PATTERN + | SearchParamsKeys.KEY_PATTERN + | SearchParamsKeys.LOGICAL_DATE_GTE + | SearchParamsKeys.LOGICAL_DATE_LTE + | SearchParamsKeys.MAP_INDEX + | SearchParamsKeys.RUN_AFTER_GTE + | SearchParamsKeys.RUN_AFTER_LTE + | SearchParamsKeys.RUN_ID_PATTERN + | SearchParamsKeys.TASK_ID_PATTERN; + +export const useFiltersHandler = (searchParamKeys: Array) => { + const { getFilterConfig } = useFilterConfigs(); + + const filterConfigs = useMemo( + () => searchParamKeys.map((key) => getFilterConfig(key)), + [searchParamKeys, getFilterConfig], + ); const [searchParams, setSearchParams] = useSearchParams(); const { setTableURLState, tableURLState } = useTableURLState(); const { pagination, sorting } = tableURLState; @@ -49,6 +68,7 @@ export const useFiltersHandler = (filterConfigs: Array) => { ); return { + filterConfigs, handleFiltersChange, searchParams, }; From 18000a8f6a95eb101a3212deab6c37d5a7a7d915 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=96=86=E5=AE=B8?= Date: Tue, 2 Sep 2025 01:56:52 -0400 Subject: [PATCH 08/14] refactor: simplify DateFilter with dayjs --- .../src/components/FilterBar/filters/DateFilter.tsx | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateFilter.tsx b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateFilter.tsx index cd0d0f1a34541..6bd7b02a8ddfd 100644 --- a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateFilter.tsx +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateFilter.tsx @@ -16,6 +16,8 @@ * specific language governing permissions and limitations * under the License. */ +import dayjs from "dayjs"; + import { DateTimeInput } from "src/components/DateTimeInput"; import { FilterPill } from "../FilterPill"; @@ -23,15 +25,7 @@ import type { FilterPluginProps } from "../types"; export const DateFilter = ({ filter, onChange, onRemove }: FilterPluginProps) => { const hasValue = filter.value !== null && filter.value !== undefined && String(filter.value).trim() !== ""; - const displayValue = hasValue - ? new Date(String(filter.value)).toLocaleString("en-US", { - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - month: "short", - year: "numeric", - }) - : ""; + const displayValue = hasValue ? dayjs(String(filter.value)).format("MMM DD, YYYY, hh:mm A") : ""; const handleDateChange = (event: React.ChangeEvent) => { const { value } = event.target; From 8a9f9a73ae57525ab6fb35cd8c7d7991c5116f0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=96=86=E5=AE=B8?= Date: Wed, 3 Sep 2025 11:43:33 -0400 Subject: [PATCH 09/14] fix(ui): Add FilterTypes enum to avoid type casting --- .../ui/src/constants/filterConfigs.tsx | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx b/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx index bbceed5fa539c..c70fdb19bd0d8 100644 --- a/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx +++ b/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx @@ -27,6 +27,12 @@ import type { FilterConfig } from "src/components/FilterBar"; import { SearchParamsKeys } from "./searchParams"; +export enum FilterTypes { + DATE = "date", + NUMBER = "number", + TEXT = "text", +} + export const useFilterConfigs = () => { const { t: translate } = useTranslation(["browse", "common", "admin"]); @@ -36,58 +42,58 @@ export const useFilterConfigs = () => { icon: , label: translate("common:dagName"), placeholder: translate("common:filters.dagDisplayNamePlaceholder"), - type: "text" as const, + type: FilterTypes.TEXT, }, [SearchParamsKeys.KEY_PATTERN]: { icon: , label: translate("admin:columns.key"), placeholder: translate("common:filters.keyPlaceholder"), - type: "text" as const, + type: FilterTypes.TEXT, }, [SearchParamsKeys.LOGICAL_DATE_GTE]: { icon: , label: translate("common:filters.logicalDateFromPlaceholder"), placeholder: translate("common:filters.logicalDateFromPlaceholder"), - type: "date" as const, + type: FilterTypes.DATE, }, [SearchParamsKeys.LOGICAL_DATE_LTE]: { icon: , label: translate("common:filters.logicalDateToPlaceholder"), placeholder: translate("common:filters.logicalDateToPlaceholder"), - type: "date" as const, + type: FilterTypes.DATE, }, [SearchParamsKeys.MAP_INDEX]: { icon: , label: translate("common:mapIndex"), min: -1, placeholder: translate("common:filters.mapIndexPlaceholder"), - type: "number" as const, + type: FilterTypes.NUMBER, }, [SearchParamsKeys.RUN_AFTER_GTE]: { icon: , label: translate("common:filters.runAfterFromPlaceholder"), placeholder: translate("common:filters.runAfterFromPlaceholder"), - type: "date" as const, + type: FilterTypes.DATE, }, [SearchParamsKeys.RUN_AFTER_LTE]: { icon: , label: translate("common:filters.runAfterToPlaceholder"), placeholder: translate("common:filters.runAfterToPlaceholder"), - type: "date" as const, + type: FilterTypes.DATE, }, [SearchParamsKeys.RUN_ID_PATTERN]: { hotkeyDisabled: true, icon: , label: translate("common:runId"), placeholder: translate("common:filters.runIdPlaceholder"), - type: "text" as const, + type: FilterTypes.TEXT, }, [SearchParamsKeys.TASK_ID_PATTERN]: { hotkeyDisabled: true, icon: , label: translate("common:taskId"), placeholder: translate("common:filters.taskIdPlaceholder"), - type: "text" as const, + type: FilterTypes.TEXT, }, }; From 160c77bb665d26817791e8aaa71d58de6b60a8b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=96=86=E5=AE=B8?= Date: Wed, 3 Sep 2025 12:13:09 -0400 Subject: [PATCH 10/14] fix(i18n): add missing translation keys --- .../src/airflow/ui/src/components/FilterBar/FilterBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/FilterBar.tsx b/airflow-core/src/airflow/ui/src/components/FilterBar/FilterBar.tsx index e98c5ce780402..9a85f777871d7 100644 --- a/airflow-core/src/airflow/ui/src/components/FilterBar/FilterBar.tsx +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/FilterBar.tsx @@ -164,7 +164,7 @@ export const FilterBar = ({ {filters.length > 0 && ( )} From 962566a8e01f6c5ee330b2962323886e867082a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=96=86=E5=AE=B8?= Date: Thu, 4 Sep 2025 11:46:13 -0400 Subject: [PATCH 11/14] fix(i18n): move filter translation key to free exemptions --- airflow-core/src/airflow/ui/public/i18n/locales/en/common.json | 1 - .../src/airflow/ui/public/i18n/locales/zh-TW/common.json | 1 - 2 files changed, 2 deletions(-) diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json index 9926fc8e1697c..3c7bc375e4369 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json @@ -102,7 +102,6 @@ }, "filters": { "dagDisplayNamePlaceholder": "Filter by Dag", - "filter": "Filter", "keyPlaceholder": "Filter by XCom key", "logicalDateFromPlaceholder": "Logical Date From", "logicalDateToPlaceholder": "Logical Date To", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/common.json index 434e944d31dd3..880388c912951 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/common.json @@ -101,7 +101,6 @@ }, "filters": { "dagDisplayNamePlaceholder": "依 Dag 名稱篩選", - "filter": "篩選", "keyPlaceholder": "依 XCom 鍵篩選", "logicalDateFromPlaceholder": "邏輯日期起始", "logicalDateToPlaceholder": "邏輯日期結束", From 710c33dbeeac12f590eb4c9a2d8a8dc94fe82976 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=96=86=E5=AE=B8?= Date: Tue, 9 Sep 2025 13:47:12 -0400 Subject: [PATCH 12/14] fix(i18n): Replace dagName translation with dag ID --- airflow-core/src/airflow/ui/public/i18n/locales/en/common.json | 1 - .../src/airflow/ui/public/i18n/locales/zh-TW/common.json | 1 - airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json index 3c7bc375e4369..defe64cce63c1 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json @@ -53,7 +53,6 @@ "tags": "Tags" }, "dagId": "Dag ID", - "dagName": "Dag Name", "dagRun": { "conf": "Conf", "dagVersions": "Dag Version(s)", diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/common.json index 880388c912951..cf89c80b5569b 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/common.json @@ -52,7 +52,6 @@ "tags": "標籤" }, "dagId": "Dag ID", - "dagName": "Dag 名稱", "dagRun": { "conf": "設定", "dagVersions": "Dag 版本", diff --git a/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx b/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx index c70fdb19bd0d8..584e767d71f19 100644 --- a/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx +++ b/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx @@ -40,7 +40,7 @@ export const useFilterConfigs = () => { [SearchParamsKeys.DAG_DISPLAY_NAME_PATTERN]: { hotkeyDisabled: true, icon: , - label: translate("common:dagName"), + label: translate("common:dagId"), placeholder: translate("common:filters.dagDisplayNamePlaceholder"), type: FilterTypes.TEXT, }, From 7ba17140d132b2b2ed2cbb8f48b201839ba7e33b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=96=86=E5=AE=B8?= Date: Tue, 9 Sep 2025 14:15:08 -0400 Subject: [PATCH 13/14] fix(i18n): simplify filter translations with fallbacks --- .../ui/public/i18n/locales/en/common.json | 13 +------------ .../ui/src/components/FilterBar/FilterBar.tsx | 2 +- .../airflow/ui/src/constants/filterConfigs.tsx | 17 ++++------------- 3 files changed, 6 insertions(+), 26 deletions(-) diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json index defe64cce63c1..b79bb59e36471 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json @@ -99,18 +99,7 @@ "any": "Any", "or": "OR" }, - "filters": { - "dagDisplayNamePlaceholder": "Filter by Dag", - "keyPlaceholder": "Filter by XCom key", - "logicalDateFromPlaceholder": "Logical Date From", - "logicalDateToPlaceholder": "Logical Date To", - "mapIndexPlaceholder": "Filter by Map Index", - "runAfterFromPlaceholder": "Run After From", - "runAfterToPlaceholder": "Run After To", - "runIdPlaceholder": "Filter by Run ID", - "taskIdPlaceholder": "Filter by Task ID", - "triggeringUserPlaceholder": "Filter by triggering user" - }, + "filter": "Filter", "logicalDate": "Logical Date", "logout": "Logout", "logoutConfirmation": "You are about to logout from the application.", diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/FilterBar.tsx b/airflow-core/src/airflow/ui/src/components/FilterBar/FilterBar.tsx index 9a85f777871d7..db009de7752d0 100644 --- a/airflow-core/src/airflow/ui/src/components/FilterBar/FilterBar.tsx +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/FilterBar.tsx @@ -146,7 +146,7 @@ export const FilterBar = ({ variant="outline" > - {translate("common:filters.filter")} + {translate("common:filter")} diff --git a/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx b/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx index 584e767d71f19..fa2913f4a03d2 100644 --- a/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx +++ b/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx @@ -41,58 +41,49 @@ export const useFilterConfigs = () => { hotkeyDisabled: true, icon: , label: translate("common:dagId"), - placeholder: translate("common:filters.dagDisplayNamePlaceholder"), type: FilterTypes.TEXT, }, [SearchParamsKeys.KEY_PATTERN]: { icon: , label: translate("admin:columns.key"), - placeholder: translate("common:filters.keyPlaceholder"), type: FilterTypes.TEXT, }, [SearchParamsKeys.LOGICAL_DATE_GTE]: { icon: , - label: translate("common:filters.logicalDateFromPlaceholder"), - placeholder: translate("common:filters.logicalDateFromPlaceholder"), + label: translate("common:filters.logicalDateFromLabel", "Logical date from"), // TODO: delete the fallback after the translation freeze type: FilterTypes.DATE, }, [SearchParamsKeys.LOGICAL_DATE_LTE]: { icon: , - label: translate("common:filters.logicalDateToPlaceholder"), - placeholder: translate("common:filters.logicalDateToPlaceholder"), + label: translate("common:filters.logicalDateToLabel", "Logical date to"), // TODO: delete the fallback after the translation freeze type: FilterTypes.DATE, }, [SearchParamsKeys.MAP_INDEX]: { icon: , label: translate("common:mapIndex"), min: -1, - placeholder: translate("common:filters.mapIndexPlaceholder"), type: FilterTypes.NUMBER, }, [SearchParamsKeys.RUN_AFTER_GTE]: { icon: , - label: translate("common:filters.runAfterFromPlaceholder"), - placeholder: translate("common:filters.runAfterFromPlaceholder"), + label: translate("common:filters.runAfterFromLabel", "Run after from"), // TODO: delete the fallback after the translation freeze type: FilterTypes.DATE, }, [SearchParamsKeys.RUN_AFTER_LTE]: { icon: , - label: translate("common:filters.runAfterToPlaceholder"), - placeholder: translate("common:filters.runAfterToPlaceholder"), + label: translate("common:filters.runAfterToLabel", "Run after to"), // TODO: delete the fallback after the translation freeze type: FilterTypes.DATE, }, [SearchParamsKeys.RUN_ID_PATTERN]: { hotkeyDisabled: true, icon: , label: translate("common:runId"), - placeholder: translate("common:filters.runIdPlaceholder"), type: FilterTypes.TEXT, }, [SearchParamsKeys.TASK_ID_PATTERN]: { hotkeyDisabled: true, icon: , label: translate("common:taskId"), - placeholder: translate("common:filters.taskIdPlaceholder"), type: FilterTypes.TEXT, }, }; From e9f58b81861c4f2492c51aa9edecdd40c8105b07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=96=86=E5=AE=B8?= Date: Wed, 10 Sep 2025 11:00:14 -0400 Subject: [PATCH 14/14] fix: modify reset button translation key --- .../src/airflow/ui/src/components/FilterBar/FilterBar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/FilterBar.tsx b/airflow-core/src/airflow/ui/src/components/FilterBar/FilterBar.tsx index db009de7752d0..b7bd83a35ba7e 100644 --- a/airflow-core/src/airflow/ui/src/components/FilterBar/FilterBar.tsx +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/FilterBar.tsx @@ -40,7 +40,7 @@ export const FilterBar = ({ maxVisibleFilters = 10, onFiltersChange, }: FilterBarProps) => { - const { t: translate } = useTranslation(); + const { t: translate } = useTranslation(["admin", "common"]); const [filters, setFilters] = useState>(() => Object.entries(initialValues) .filter(([, value]) => value !== null && value !== undefined && value !== "") @@ -164,7 +164,7 @@ export const FilterBar = ({ {filters.length > 0 && ( )}