This is an automated email from the ASF dual-hosted git repository.

kaxilnaik pushed a commit to branch v3-1-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit 73ff74a48480e570420784f74d8475aa412e5800
Author: LI,JHE-CHEN <[email protected]>
AuthorDate: Wed Sep 10 12:08:17 2025 -0400

    Add reusable FilterBar component for date, number, text input (#54895)
    
    * feat(ui): add reusable FilterBar component for date, number, text input
    
    * fix(ui): improve FilterPill focus and blur handling
    
    * refactor(ui): create useFiltersHandler hook for FilterBar
    
    * refactor(ui):Support negative number input & fix FilterPill DOM nesting
    
    * feat(ui): create centralized filter config
    
    * fix(ui): apply style feedback to FilterBar
    
    * refactor(ui): move getFilterConfig calls into useFiltersHandler & remove 
placeholder translation fallback
    
    * refactor: simplify DateFilter with dayjs
    
    * fix(ui): Add FilterTypes enum to avoid type casting
    
    * fix(i18n): add missing translation keys
    
    * fix(i18n): move filter translation key to free exemptions
    
    * fix(i18n): Replace dagName translation with dag ID
    
    * fix(i18n): simplify filter translations with fallbacks
    
    * fix: modify reset button translation key
    
    ---------
    
    Co-authored-by: Brent Bovenzi <[email protected]>
    (cherry picked from commit 2a1c68684cc0501f1a8e279bfc6ad5fe409906a4)
---
 .../airflow/ui/public/i18n/locales/en/common.json  |  13 +-
 .../ui/src/components/FilterBar/FilterBar.tsx      | 172 +++++++++++++++
 .../ui/src/components/FilterBar/FilterPill.tsx     | 161 ++++++++++++++
 .../FilterBar/defaultIcons.tsx}                    |  15 +-
 .../components/FilterBar/filters/DateFilter.tsx    |  54 +++++
 .../components/FilterBar/filters/NumberFilter.tsx  | 112 ++++++++++
 .../FilterBar/filters/TextSearchFilter.tsx         |  63 ++++++
 .../src/{utils => components/FilterBar}/index.ts   |  13 +-
 .../airflow/ui/src/components/FilterBar/types.ts   |  53 +++++
 .../ui/src/components/ui/InputWithAddon.tsx        |  68 ++++++
 .../src/airflow/ui/src/components/ui/index.ts      |   1 +
 .../src/airflow/ui/src/constants/filterConfigs.tsx |  97 +++++++++
 .../src/airflow/ui/src/pages/XCom/XComFilters.tsx  | 236 +++++----------------
 airflow-core/src/airflow/ui/src/utils/index.ts     |   1 +
 .../src/airflow/ui/src/utils/useFiltersHandler.ts  |  75 +++++++
 15 files changed, 927 insertions(+), 207 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 defe64cce63..b79bb59e364 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
new file mode 100644
index 00000000000..b7bd83a35ba
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/components/FilterBar/FilterBar.tsx
@@ -0,0 +1,172 @@
+/*!
+ * 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<string, FilterValue> = {};
+
+const getFilterIcon = (config: FilterConfig) => config.icon ?? 
getDefaultFilterIcon(config.type);
+
+export const FilterBar = ({
+  configs,
+  initialValues = defaultInitialValues,
+  maxVisibleFilters = 10,
+  onFiltersChange,
+}: FilterBarProps) => {
+  const { t: translate } = useTranslation(["admin", "common"]);
+  const [filters, setFilters] = useState<Array<FilterState>>(() =>
+    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<string, FilterValue>) => {
+    onFiltersChange(filtersRecord);
+  }, 100);
+
+  const updateFiltersRecord = useCallback(
+    (updatedFilters: Array<FilterState>) => {
+      const filtersRecord = updatedFilters.reduce<Record<string, 
FilterValue>>((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 <DateFilter key={filter.id} {...props} />;
+      case "number":
+        return <NumberFilter key={filter.id} {...props} />;
+      case "text":
+        return <TextSearchFilter key={filter.id} {...props} />;
+      default:
+        return undefined;
+    }
+  };
+
+  return (
+    <HStack gap={2} wrap="wrap">
+      {filters.slice(0, maxVisibleFilters).map(renderFilter)}
+      {availableConfigs.length > 0 && (
+        <Menu.Root>
+          <Menu.Trigger asChild>
+            <Button
+              _hover={{ bg: "colorPalette.subtle" }}
+              bg="gray.muted"
+              borderRadius="full"
+              variant="outline"
+            >
+              <MdAdd />
+              {translate("common:filter")}
+            </Button>
+          </Menu.Trigger>
+          <Menu.Content>
+            {availableConfigs.map((config) => (
+              <Menu.Item key={config.key} onClick={() => addFilter(config)} 
value={config.key}>
+                <HStack gap={2}>
+                  {getFilterIcon(config)}
+                  {config.label}
+                </HStack>
+              </Menu.Item>
+            ))}
+          </Menu.Content>
+        </Menu.Root>
+      )}
+      {filters.length > 0 && (
+        <Button borderRadius="full" colorPalette="gray" onClick={resetFilters} 
size="sm" variant="outline">
+          <MdClear />
+          {translate("admin:formActions.reset")}
+        </Button>
+      )}
+    </HStack>
+  );
+};
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 00000000000..f8ce033a3e0
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/components/FilterBar/FilterPill.tsx
@@ -0,0 +1,161 @@
+/*!
+ * 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, HStack } from "@chakra-ui/react";
+import React from "react";
+import { useEffect, useRef, 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 inputRef = useRef<HTMLInputElement>(null);
+  const blurTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
+
+  const handlePillClick = () => setIsEditing(true);
+
+  const handleKeyDown = (event: React.KeyboardEvent) => {
+    if (event.key === "Enter" || event.key === "Escape") {
+      setIsEditing(false);
+    }
+  };
+
+  const handleBlur = () => {
+    blurTimeoutRef.current = setTimeout(() => setIsEditing(false), 150);
+  };
+
+  const handleFocus = () => {
+    if (blurTimeoutRef.current) {
+      clearTimeout(blurTimeoutRef.current);
+      blurTimeoutRef.current = undefined;
+    }
+  };
+
+  useEffect(() => {
+    if (isEditing && inputRef.current) {
+      const input = inputRef.current;
+      const focusInput = () => {
+        input.focus();
+        try {
+          input.select();
+        } catch {
+          // NumberInputField doesn't support select()
+        }
+      };
+
+      requestAnimationFrame(focusInput);
+    }
+  }, [isEditing]);
+
+  useEffect(
+    () => () => {
+      if (blurTimeoutRef.current) {
+        clearTimeout(blurTimeoutRef.current);
+      }
+    },
+    [],
+  );
+
+  const childrenWithProps = React.Children.map(children, (child) => {
+    if (React.isValidElement(child)) {
+      return React.cloneElement(child, {
+        onBlur: handleBlur,
+        onChange,
+        onFocus: handleFocus,
+        onKeyDown: handleKeyDown,
+        ref: inputRef,
+        ...child.props,
+      } as Record<string, unknown>);
+    }
+
+    return child;
+  });
+
+  if (isEditing) {
+    return childrenWithProps;
+  }
+
+  return (
+    <Box
+      _hover={{ bg: "colorPalette.subtle" }}
+      as="button"
+      bg={hasValue ? "blue.muted" : "gray.muted"}
+      borderRadius="full"
+      color="colorPalette.fg"
+      colorPalette={hasValue ? "blue" : "gray"}
+      cursor="pointer"
+      display="flex"
+      fontSize="sm"
+      fontWeight="medium"
+      h="10"
+      onClick={handlePillClick}
+      px={4}
+    >
+      <HStack align="center" gap={1}>
+        {filter.config.icon ?? getDefaultFilterIcon(filter.config.type)}
+        <Box flex="1" px={2} py={2}>
+          {filter.config.label}: {displayValue}
+        </Box>
+
+        <Box
+          _hover={{
+            bg: "gray.100",
+            color: "gray.600",
+          }}
+          alignItems="center"
+          aria-label={`Remove ${filter.config.label} filter`}
+          bg="transparent"
+          borderRadius="full"
+          color="gray.400"
+          cursor="pointer"
+          display="flex"
+          h={6}
+          justifyContent="center"
+          mr={1}
+          onClick={(event) => {
+            event.stopPropagation();
+            onRemove();
+          }}
+          transition="all 0.2s"
+          w={6}
+        >
+          <MdClose size={16} />
+        </Box>
+      </HStack>
+    </Box>
+  );
+};
diff --git a/airflow-core/src/airflow/ui/src/utils/index.ts 
b/airflow-core/src/airflow/ui/src/components/FilterBar/defaultIcons.tsx
similarity index 70%
copy from airflow-core/src/airflow/ui/src/utils/index.ts
copy to airflow-core/src/airflow/ui/src/components/FilterBar/defaultIcons.tsx
index d1badfb8f00..bb622ecbd91 100644
--- a/airflow-core/src/airflow/ui/src/utils/index.ts
+++ b/airflow-core/src/airflow/ui/src/components/FilterBar/defaultIcons.tsx
@@ -16,9 +16,14 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+import { MdCalendarToday, MdNumbers, MdTextFields } from "react-icons/md";
 
-export { capitalize } from "./capitalize";
-export { getDuration, renderDuration } from "./datetimeUtils";
-export { getMetaKey } from "./getMetaKey";
-export { useContainerWidth } from "./useContainerWidth";
-export * from "./query";
+import type { FilterConfig } from "./types";
+
+export const defaultFilterIcons = {
+  date: <MdCalendarToday />,
+  number: <MdNumbers />,
+  text: <MdTextFields />,
+} 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 00000000000..6bd7b02a8dd
--- /dev/null
+++ 
b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateFilter.tsx
@@ -0,0 +1,54 @@
+/*!
+ * 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 dayjs from "dayjs";
+
+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 ? dayjs(String(filter.value)).format("MMM DD, 
YYYY, hh:mm A") : "";
+
+  const handleDateChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+    const { value } = event.target;
+
+    onChange(value || undefined);
+  };
+
+  return (
+    <FilterPill
+      displayValue={displayValue}
+      filter={filter}
+      hasValue={hasValue}
+      onChange={onChange}
+      onRemove={onRemove}
+    >
+      <DateTimeInput
+        borderRadius="full"
+        onChange={handleDateChange}
+        placeholder={filter.config.placeholder}
+        size="sm"
+        value={filter.value !== null && filter.value !== undefined ? 
String(filter.value) : ""}
+        width="200px"
+      />
+    </FilterPill>
+  );
+};
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 00000000000..d8a89895c0d
--- /dev/null
+++ 
b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/NumberFilter.tsx
@@ -0,0 +1,112 @@
+/*!
+ * 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 { 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 (
+    <NumberInputRoot
+      borderRadius="full"
+      max={max}
+      min={min}
+      onValueChange={onValueChange}
+      overflow="hidden"
+      value={value}
+      width="180px"
+    >
+      <NumberInputField
+        borderRadius="full"
+        onBlur={onBlur}
+        onFocus={onFocus}
+        onKeyDown={onKeyDown}
+        placeholder={placeholder}
+        ref={ref}
+      />
+    </NumberInputRoot>
+  );
+});
+
+NumberInputWithRef.displayName = "NumberInputWithRef";
+
+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;
+    }
+
+    // Allow user to input negative sign for negative number
+    if (value === "-") {
+      return;
+    }
+
+    const parsedValue = Number(value);
+
+    if (!isNaN(parsedValue)) {
+      onChange(parsedValue);
+    }
+  };
+
+  return (
+    <FilterPill
+      displayValue={hasValue ? String(filter.value) : ""}
+      filter={filter}
+      hasValue={hasValue}
+      onChange={onChange}
+      onRemove={onRemove}
+    >
+      <NumberInputWithRef
+        max={filter.config.max}
+        min={filter.config.min}
+        onValueChange={handleValueChange}
+        placeholder={filter.config.placeholder}
+        value={inputValue}
+      />
+    </FilterPill>
+  );
+};
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 00000000000..8cca8cf0853
--- /dev/null
+++ 
b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/TextSearchFilter.tsx
@@ -0,0 +1,63 @@
+/*!
+ * 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 { useRef } from "react";
+import { useHotkeys } from "react-hotkeys-hook";
+
+import { InputWithAddon } from "../../ui";
+import { FilterPill } from "../FilterPill";
+import type { FilterPluginProps } from "../types";
+
+export const TextSearchFilter = ({ filter, onChange, onRemove }: 
FilterPluginProps) => {
+  const inputRef = useRef<HTMLInputElement>(null);
+
+  const hasValue = filter.value !== null && filter.value !== undefined && 
String(filter.value).trim() !== "";
+
+  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+    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 (
+    <FilterPill
+      displayValue={hasValue ? String(filter.value) : ""}
+      filter={filter}
+      hasValue={hasValue}
+      onChange={onChange}
+      onRemove={onRemove}
+    >
+      <InputWithAddon
+        label={filter.config.label}
+        onChange={handleInputChange}
+        placeholder={filter.config.placeholder}
+        value={String(filter.value ?? "")}
+      />
+    </FilterPill>
+  );
+};
diff --git a/airflow-core/src/airflow/ui/src/utils/index.ts 
b/airflow-core/src/airflow/ui/src/components/FilterBar/index.ts
similarity index 69%
copy from airflow-core/src/airflow/ui/src/utils/index.ts
copy to airflow-core/src/airflow/ui/src/components/FilterBar/index.ts
index d1badfb8f00..fa3c5c7621d 100644
--- a/airflow-core/src/airflow/ui/src/utils/index.ts
+++ b/airflow-core/src/airflow/ui/src/components/FilterBar/index.ts
@@ -16,9 +16,10 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-
-export { capitalize } from "./capitalize";
-export { getDuration, renderDuration } from "./datetimeUtils";
-export { getMetaKey } from "./getMetaKey";
-export { useContainerWidth } from "./useContainerWidth";
-export * from "./query";
+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 00000000000..0b3820e5c71
--- /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<FilterConfig>;
+  readonly initialValues?: Record<string, FilterValue>;
+  readonly maxVisibleFilters?: number;
+  readonly onFiltersChange: (filters: Record<string, FilterValue>) => void;
+};
+
+export type FilterPluginProps = {
+  readonly filter: FilterState;
+  readonly onChange: (value: FilterValue) => void;
+  readonly onRemove: () => void;
+};
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 00000000000..b5a49e6cdf8
--- /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<HTMLInputElement, 
InputWithAddonProps>((props, ref) => {
+  const { label, width = "220px", ...inputProps } = props;
+
+  return (
+    <Box
+      alignItems="center"
+      bg="bg"
+      border="1px solid"
+      borderColor="border"
+      borderRadius="full"
+      display="flex"
+      width={width}
+    >
+      <Text
+        bg="gray.muted"
+        borderLeftRadius="full"
+        color="colorPalette.fg"
+        colorPalette="gray"
+        fontSize="sm"
+        fontWeight="medium"
+        px={3}
+        py={2}
+        whiteSpace="nowrap"
+      >
+        {label}:
+      </Text>
+      <Input
+        bg="transparent"
+        border="none"
+        borderRadius="0"
+        flex="1"
+        outline="none"
+        ref={ref}
+        size="sm"
+        {...inputProps}
+      />
+    </Box>
+  );
+});
+
+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 b7695029c4e..e39d15dd09d 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
new file mode 100644
index 00000000000..fa2913f4a03
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx
@@ -0,0 +1,97 @@
+/*!
+ * 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 { useTranslation } from "react-i18next";
+import { FiBarChart } from "react-icons/fi";
+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";
+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"]);
+
+  const filterConfigMap = {
+    [SearchParamsKeys.DAG_DISPLAY_NAME_PATTERN]: {
+      hotkeyDisabled: true,
+      icon: <DagIcon />,
+      label: translate("common:dagId"),
+      type: FilterTypes.TEXT,
+    },
+    [SearchParamsKeys.KEY_PATTERN]: {
+      icon: <MdSearch />,
+      label: translate("admin:columns.key"),
+      type: FilterTypes.TEXT,
+    },
+    [SearchParamsKeys.LOGICAL_DATE_GTE]: {
+      icon: <MdDateRange />,
+      label: translate("common:filters.logicalDateFromLabel", "Logical date 
from"), // TODO: delete the fallback after the translation freeze
+      type: FilterTypes.DATE,
+    },
+    [SearchParamsKeys.LOGICAL_DATE_LTE]: {
+      icon: <MdDateRange />,
+      label: translate("common:filters.logicalDateToLabel", "Logical date 
to"), // TODO: delete the fallback after the translation freeze
+      type: FilterTypes.DATE,
+    },
+    [SearchParamsKeys.MAP_INDEX]: {
+      icon: <LuBrackets />,
+      label: translate("common:mapIndex"),
+      min: -1,
+      type: FilterTypes.NUMBER,
+    },
+    [SearchParamsKeys.RUN_AFTER_GTE]: {
+      icon: <MdDateRange />,
+      label: translate("common:filters.runAfterFromLabel", "Run after from"), 
// TODO: delete the fallback after the translation freeze
+      type: FilterTypes.DATE,
+    },
+    [SearchParamsKeys.RUN_AFTER_LTE]: {
+      icon: <MdDateRange />,
+      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: <FiBarChart />,
+      label: translate("common:runId"),
+      type: FilterTypes.TEXT,
+    },
+    [SearchParamsKeys.TASK_ID_PATTERN]: {
+      hotkeyDisabled: true,
+      icon: <TaskIcon />,
+      label: translate("common:taskId"),
+      type: FilterTypes.TEXT,
+    },
+  };
+
+  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 c7f36cfdd1c..4b7fec38b28 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,74 @@
  * 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 { useTranslation } from "react-i18next";
-import { LuX } from "react-icons/lu";
-import { useSearchParams, useParams } from "react-router-dom";
+import { VStack } from "@chakra-ui/react";
+import { useMemo } from "react";
+import { useParams } from "react-router-dom";
 
-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 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";
-}>;
+import { useFiltersHandler, type FilterableSearchParamsKeys } 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"]);
-  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 searchParamKeys = useMemo((): Array<FilterableSearchParamsKeys> => {
+    const keys: Array<FilterableSearchParamsKeys> = [
+      SearchParamsKeys.KEY_PATTERN,
+      SearchParamsKeys.LOGICAL_DATE_GTE,
+      SearchParamsKeys.LOGICAL_DATE_LTE,
+      SearchParamsKeys.RUN_AFTER_GTE,
+      SearchParamsKeys.RUN_AFTER_LTE,
+    ];
 
-  const handleFilterChange = useCallback(
-    (paramKey: string) => (value: string) => {
-      if (value === "") {
-        searchParams.delete(paramKey);
-      } else {
-        searchParams.set(paramKey, value);
-      }
-      setTableURLState({
-        pagination: { ...pagination, pageIndex: 0 },
-        sorting,
-      });
-      setSearchParams(searchParams);
-    },
-    [pagination, searchParams, setSearchParams, setTableURLState, sorting],
-  );
+    if (dagId === "~") {
+      keys.push(SearchParamsKeys.DAG_DISPLAY_NAME_PATTERN);
+    }
 
-  const filterCount = useMemo(
-    () =>
-      visibleFilters.filter((filter) => {
-        const value = searchParams.get(filter.key);
+    if (runId === "~") {
+      keys.push(SearchParamsKeys.RUN_ID_PATTERN);
+    }
 
-        return value !== null && value !== "";
-      }).length,
-    [searchParams, visibleFilters],
-  );
+    if (taskId === "~") {
+      keys.push(SearchParamsKeys.TASK_ID_PATTERN);
+    }
 
-  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]);
+    if (mapIndex === "-1") {
+      keys.push(SearchParamsKeys.MAP_INDEX);
+    }
 
-  const renderFilterInput = (filter: (typeof FILTERS)[number]) => {
-    const { key, translationKey, type } = filter;
+    return keys;
+  }, [dagId, mapIndex, runId, taskId]);
 
-    return (
-      <Box key={key} w="200px">
-        <Box marginBottom={1} minHeight="1.2em">
-          {type !== "search" && <Text 
fontSize="xs">{translate(`common:filters.${translationKey}`)}</Text>}
-        </Box>
-        {type === "search" ? (
-          (() => {
-            const { hotkeyDisabled } = filter;
+  const { filterConfigs, handleFiltersChange, searchParams } = 
useFiltersHandler(searchParamKeys);
+
+  const initialValues = useMemo(() => {
+    const values: Record<string, FilterValue> = {};
+
+    filterConfigs.forEach((config) => {
+      const value = searchParams.get(config.key);
+
+      if (value !== null && value !== "") {
+        if (config.type === "number") {
+          const parsedValue = Number(value);
+
+          values[config.key] = isNaN(parsedValue) ? value : parsedValue;
+        } else {
+          values[config.key] = value;
+        }
+      }
+    });
 
-            return (
-              <SearchBar
-                defaultValue={searchParams.get(key) ?? ""}
-                hideAdvanced
-                hotkeyDisabled={hotkeyDisabled}
-                key={`${key}-${resetKey}`}
-                onChange={handleFilterChange(key)}
-                placeHolder={translate(`common:filters.${translationKey}`)}
-              />
-            );
-          })()
-        ) : type === "datetime" ? (
-          <DateTimeInput
-            key={`${key}-${resetKey}`}
-            onChange={(event) => handleFilterChange(key)(event.target.value)}
-            value={searchParams.get(key) ?? ""}
-          />
-        ) : (
-          <NumberInputRoot
-            key={`${key}-${resetKey}`}
-            min={-1}
-            onValueChange={(details) => handleFilterChange(key)(details.value)}
-            value={searchParams.get(key) ?? ""}
-          >
-            <NumberInputField 
placeholder={translate(`common:filters.${translationKey}`)} />
-          </NumberInputRoot>
-        )}
-      </Box>
-    );
-  };
+    return values;
+  }, [searchParams, filterConfigs]);
 
   return (
     <VStack align="start" gap={4} paddingY="4px">
-      <HStack flexWrap="wrap" gap={4}>
-        {visibleFilters.map(renderFilterInput)}
-        <Box>
-          <Text fontSize="xs" marginBottom={1}>
-            &nbsp;
-          </Text>
-          {filterCount > 0 && (
-            <Button onClick={handleResetFilters} size="md" variant="outline">
-              <LuX />
-              {translate("common:table.filterReset", { count: filterCount })}
-            </Button>
-          )}
-        </Box>
-      </HStack>
+      <FilterBar
+        configs={filterConfigs}
+        initialValues={initialValues}
+        onFiltersChange={handleFiltersChange}
+      />
     </VStack>
   );
 };
diff --git a/airflow-core/src/airflow/ui/src/utils/index.ts 
b/airflow-core/src/airflow/ui/src/utils/index.ts
index d1badfb8f00..c8d15c7cdb5 100644
--- a/airflow-core/src/airflow/ui/src/utils/index.ts
+++ b/airflow-core/src/airflow/ui/src/utils/index.ts
@@ -21,4 +21,5 @@ export { capitalize } from "./capitalize";
 export { getDuration, renderDuration } from "./datetimeUtils";
 export { getMetaKey } from "./getMetaKey";
 export { useContainerWidth } from "./useContainerWidth";
+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
new file mode 100644
index 00000000000..9f4a595d85a
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/utils/useFiltersHandler.ts
@@ -0,0 +1,75 @@
+/*!
+ * 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 { useCallback, useMemo } from "react";
+import { useSearchParams } from "react-router-dom";
+
+import { useTableURLState } from "src/components/DataTable/useTableUrlState";
+import type { FilterValue } from "src/components/FilterBar";
+import { useFilterConfigs } from "src/constants/filterConfigs";
+import type { SearchParamsKeys } from "src/constants/searchParams";
+
+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<FilterableSearchParamsKeys>) => {
+  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;
+
+  const handleFiltersChange = useCallback(
+    (filters: Record<string, FilterValue>) => {
+      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 {
+    filterConfigs,
+    handleFiltersChange,
+    searchParams,
+  };
+};


Reply via email to