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 410f49fa0e5b81f59e26c8b10f98b654077ca79b
Author: Guan Ming(Wesley) Chiu <[email protected]>
AuthorDate: Wed Sep 17 02:07:58 2025 +0800

    Add filter for HITL TaskInstances page (#55620)
    
    * Add select filter support in FilterBar
    
    * Support React.Element for select options
    
    (cherry picked from commit 8e91ac5e59c9baa04e24cd4696d39f180d116a77)
---
 .../ui/src/components/FilterBar/FilterBar.tsx      |   3 +
 .../ui/src/components/FilterBar/FilterPill.tsx     |   4 +-
 .../ui/src/components/FilterBar/defaultIcons.tsx   |   3 +-
 .../components/FilterBar/filters/SelectFilter.tsx  | 108 +++++++++++++++++++++
 .../airflow/ui/src/components/FilterBar/types.ts   |   3 +-
 .../ui/src/components/ui/Select/Trigger.tsx        |   7 +-
 .../src/airflow/ui/src/constants/filterConfigs.tsx |  20 +++-
 .../ui/src/pages/HITLTaskInstances/HITLFilters.tsx |  84 ++++++++++++++++
 .../pages/HITLTaskInstances/HITLTaskInstances.tsx  |  88 +++++++----------
 .../src/airflow/ui/src/utils/useFiltersHandler.ts  |   1 +
 10 files changed, 260 insertions(+), 61 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 66644b57cbc..849047dde07 100644
--- a/airflow-core/src/airflow/ui/src/components/FilterBar/FilterBar.tsx
+++ b/airflow-core/src/airflow/ui/src/components/FilterBar/FilterBar.tsx
@@ -27,6 +27,7 @@ import { Menu } from "src/components/ui";
 import { getDefaultFilterIcon } from "./defaultIcons";
 import { DateFilter } from "./filters/DateFilter";
 import { NumberFilter } from "./filters/NumberFilter";
+import { SelectFilter } from "./filters/SelectFilter";
 import { TextSearchFilter } from "./filters/TextSearchFilter";
 import type { FilterBarProps, FilterConfig, FilterState, FilterValue } from 
"./types";
 
@@ -126,6 +127,8 @@ export const FilterBar = ({
         return <DateFilter key={filter.id} {...props} />;
       case "number":
         return <NumberFilter key={filter.id} {...props} />;
+      case "select":
+        return <SelectFilter key={filter.id} {...props} />;
       case "text":
         return <TextSearchFilter key={filter.id} {...props} />;
       default:
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 f8ce033a3e0..ef789617f20 100644
--- a/airflow-core/src/airflow/ui/src/components/FilterBar/FilterPill.tsx
+++ b/airflow-core/src/airflow/ui/src/components/FilterBar/FilterPill.tsx
@@ -26,7 +26,7 @@ import type { FilterState, FilterValue } from "./types";
 
 type FilterPillProps = {
   readonly children: React.ReactNode;
-  readonly displayValue: string;
+  readonly displayValue: React.ReactNode | string;
   readonly filter: FilterState;
   readonly hasValue: boolean;
   readonly onChange: (value: FilterValue) => void;
@@ -127,7 +127,7 @@ export const FilterPill = ({
     >
       <HStack align="center" gap={1}>
         {filter.config.icon ?? getDefaultFilterIcon(filter.config.type)}
-        <Box flex="1" px={2} py={2}>
+        <Box alignItems="center" display="flex" flex="1" gap={2} px={2}>
           {filter.config.label}: {displayValue}
         </Box>
 
diff --git 
a/airflow-core/src/airflow/ui/src/components/FilterBar/defaultIcons.tsx 
b/airflow-core/src/airflow/ui/src/components/FilterBar/defaultIcons.tsx
index bb622ecbd91..338b89422b7 100644
--- a/airflow-core/src/airflow/ui/src/components/FilterBar/defaultIcons.tsx
+++ b/airflow-core/src/airflow/ui/src/components/FilterBar/defaultIcons.tsx
@@ -16,13 +16,14 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { MdCalendarToday, MdNumbers, MdTextFields } from "react-icons/md";
+import { MdCalendarToday, MdNumbers, MdTextFields, MdArrowDropDown } from 
"react-icons/md";
 
 import type { FilterConfig } from "./types";
 
 export const defaultFilterIcons = {
   date: <MdCalendarToday />,
   number: <MdNumbers />,
+  select: <MdArrowDropDown />,
   text: <MdTextFields />,
 } as const;
 
diff --git 
a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/SelectFilter.tsx 
b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/SelectFilter.tsx
new file mode 100644
index 00000000000..79374b27c23
--- /dev/null
+++ 
b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/SelectFilter.tsx
@@ -0,0 +1,108 @@
+/*!
+ * 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 { createListCollection, Box, Text } from "@chakra-ui/react";
+
+import { Select } from "src/components/ui";
+
+import { FilterPill } from "../FilterPill";
+import type { FilterPluginProps, FilterConfig } from "../types";
+
+type SelectOption = {
+  label: string;
+  value: string;
+};
+
+type SelectFilterConfig = {
+  options: Array<SelectOption>;
+};
+
+export const SelectFilter = ({ filter, onChange, onRemove }: 
FilterPluginProps) => {
+  const config = filter.config as FilterConfig & SelectFilterConfig;
+
+  const handleValueChange = ({ value }: { value: Array<string> }) => {
+    const [newValue] = value;
+
+    onChange(newValue);
+
+    // Trigger blur to close the editing mode after selection
+    setTimeout(() => {
+      const activeElement = document.activeElement as HTMLElement;
+
+      activeElement.blur();
+    }, 0);
+  };
+
+  const hasValue = filter.value !== null && filter.value !== undefined && 
filter.value !== "";
+  const displayValue = config.options.find((option) => option.value === 
String(filter.value))?.label;
+
+  return (
+    <FilterPill
+      displayValue={displayValue ?? ""}
+      filter={filter}
+      hasValue={hasValue}
+      onChange={onChange}
+      onRemove={onRemove}
+    >
+      <Box
+        alignItems="center"
+        bg="bg"
+        border="0.5px solid"
+        borderColor="border"
+        borderRadius="full"
+        display="flex"
+        h="full"
+        overflow="hidden"
+        width="330px"
+      >
+        <Text
+          alignItems="center"
+          bg="gray.muted"
+          borderLeftRadius="full"
+          display="flex"
+          fontSize="sm"
+          fontWeight="medium"
+          h="full"
+          px={4}
+          py={2}
+          whiteSpace="nowrap"
+        >
+          {filter.config.label}:
+        </Text>
+        <Select.Root
+          border="none"
+          collection={createListCollection({ items: config.options })}
+          h="full"
+          onValueChange={handleValueChange}
+          value={hasValue ? [String(filter.value)] : []}
+        >
+          <Select.Trigger triggerProps={{ border: "none" }}>
+            <Select.ValueText placeholder={filter.config.placeholder} />
+          </Select.Trigger>
+          <Select.Content>
+            {config.options.map((option) => (
+              <Select.Item item={option} key={option.value}>
+                {option.label}
+              </Select.Item>
+            ))}
+          </Select.Content>
+        </Select.Root>
+      </Box>
+    </FilterPill>
+  );
+};
diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/types.ts 
b/airflow-core/src/airflow/ui/src/components/FilterBar/types.ts
index 0b3820e5c71..0f997c90671 100644
--- a/airflow-core/src/airflow/ui/src/components/FilterBar/types.ts
+++ b/airflow-core/src/airflow/ui/src/components/FilterBar/types.ts
@@ -28,9 +28,10 @@ export type FilterConfig = {
   readonly label: string;
   readonly max?: number;
   readonly min?: number;
+  readonly options?: Array<{ label: React.ReactNode | string; value: string }>;
   readonly placeholder?: string;
   readonly required?: boolean;
-  readonly type: "date" | "number" | "text";
+  readonly type: "date" | "number" | "select" | "text";
 };
 
 export type FilterState = {
diff --git a/airflow-core/src/airflow/ui/src/components/ui/Select/Trigger.tsx 
b/airflow-core/src/airflow/ui/src/components/ui/Select/Trigger.tsx
index 9a6390d1e72..ce5f47f2016 100644
--- a/airflow-core/src/airflow/ui/src/components/ui/Select/Trigger.tsx
+++ b/airflow-core/src/airflow/ui/src/components/ui/Select/Trigger.tsx
@@ -24,14 +24,17 @@ import { CloseButton } from "../CloseButton";
 type Props = {
   readonly clearable?: boolean;
   readonly isActive?: boolean;
+  readonly triggerProps?: ChakraSelect.TriggerProps;
 } & ChakraSelect.ControlProps;
 
 export const Trigger = forwardRef<HTMLButtonElement, Props>((props, ref) => {
-  const { children, clearable, isActive, ...rest } = props;
+  const { children, clearable, isActive, triggerProps, ...rest } = props;
 
   return (
     <ChakraSelect.Control {...rest}>
-      <ChakraSelect.Trigger ref={ref}>{children}</ChakraSelect.Trigger>
+      <ChakraSelect.Trigger ref={ref} {...triggerProps}>
+        {children}
+      </ChakraSelect.Trigger>
       <ChakraSelect.IndicatorGroup _rtl={{ bottom: 0, left: 0, right: "auto", 
top: 0 }}>
         {clearable ? (
           <ChakraSelect.ClearTrigger asChild>
diff --git a/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx 
b/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx
index 0dc09b1fd55..8c83a75b84d 100644
--- a/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx
+++ b/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx
@@ -24,17 +24,19 @@ 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 { StateBadge } from "src/components/StateBadge";
 
 import { SearchParamsKeys } from "./searchParams";
 
 export enum FilterTypes {
   DATE = "date",
   NUMBER = "number",
+  SELECT = "select",
   TEXT = "text",
 }
 
 export const useFilterConfigs = () => {
-  const { t: translate } = useTranslation(["browse", "common", "admin"]);
+  const { t: translate } = useTranslation(["browse", "common", "admin", 
"hitl"]);
 
   const filterConfigMap = {
     [SearchParamsKeys.AFTER]: {
@@ -89,6 +91,22 @@ export const useFilterConfigs = () => {
       min: -1,
       type: FilterTypes.NUMBER,
     },
+    [SearchParamsKeys.RESPONSE_RECEIVED]: {
+      icon: <FiUser />,
+      label: translate("hitl:requiredActionState"),
+      options: [
+        { label: translate("hitl:filters.response.all"), value: "all" },
+        {
+          label: <StateBadge 
state="deferred">{translate("hitl:filters.response.pending")}</StateBadge>,
+          value: "false",
+        },
+        {
+          label: <StateBadge 
state="success">{translate("hitl:filters.response.received")}</StateBadge>,
+          value: "true",
+        },
+      ],
+      type: FilterTypes.SELECT,
+    },
     [SearchParamsKeys.RUN_AFTER_GTE]: {
       icon: <MdDateRange />,
       label: translate("common:filters.runAfterFrom"),
diff --git 
a/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLFilters.tsx 
b/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLFilters.tsx
new file mode 100644
index 00000000000..84aec7292c8
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLFilters.tsx
@@ -0,0 +1,84 @@
+/*!
+ * 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 { VStack } from "@chakra-ui/react";
+import { useMemo } from "react";
+import { useParams, useSearchParams } from "react-router-dom";
+
+import { FilterBar, type FilterValue } from "src/components/FilterBar";
+import { SearchParamsKeys } from "src/constants/searchParams";
+import { useFiltersHandler, type FilterableSearchParamsKeys } from "src/utils";
+
+export const HITLFilters = ({ onResponseChange }: { readonly onResponseChange: 
() => void }) => {
+  const { dagId = "~", taskId = "~" } = useParams();
+  const [urlSearchParams] = useSearchParams();
+  const responseReceived = 
urlSearchParams.get(SearchParamsKeys.RESPONSE_RECEIVED);
+
+  const searchParamKeys = useMemo((): Array<FilterableSearchParamsKeys> => {
+    const keys: Array<FilterableSearchParamsKeys> = [];
+
+    if (dagId === "~") {
+      keys.push(SearchParamsKeys.DAG_DISPLAY_NAME_PATTERN);
+    }
+
+    if (taskId === "~") {
+      keys.push(SearchParamsKeys.TASK_ID_PATTERN);
+    }
+
+    keys.push(SearchParamsKeys.RESPONSE_RECEIVED);
+
+    return keys;
+  }, [dagId, taskId]);
+
+  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;
+        }
+      }
+    });
+
+    values[SearchParamsKeys.RESPONSE_RECEIVED] = responseReceived;
+
+    return values;
+  }, [filterConfigs, responseReceived, searchParams]);
+
+  return (
+    <VStack align="start" pt={2}>
+      <FilterBar
+        configs={filterConfigs}
+        initialValues={initialValues}
+        onFiltersChange={(filters) => {
+          onResponseChange();
+          handleFiltersChange(filters);
+        }}
+      />
+    </VStack>
+  );
+};
diff --git 
a/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.tsx 
b/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.tsx
index 65fb4a33db9..376c2abf37f 100644
--- 
a/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.tsx
+++ 
b/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.tsx
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Box, Heading, Link, createListCollection } from "@chakra-ui/react";
+import { Heading, Link, VStack } from "@chakra-ui/react";
 import type { ColumnDef } from "@tanstack/react-table";
 import type { TFunction } from "i18next";
 import { useCallback } from "react";
@@ -31,15 +31,20 @@ import { ErrorAlert } from "src/components/ErrorAlert";
 import { StateBadge } from "src/components/StateBadge";
 import Time from "src/components/Time";
 import { TruncatedText } from "src/components/TruncatedText";
-import { Select } from "src/components/ui";
 import { SearchParamsKeys, type SearchParamsKeysType } from 
"src/constants/searchParams";
 import { getHITLState } from "src/utils/hitl";
 import { getTaskInstanceLink } from "src/utils/links";
 
+import { HITLFilters } from "./HITLFilters";
+
 type TaskInstanceRow = { row: { original: HITLDetail } };
 
-const { OFFSET: OFFSET_PARAM, RESPONSE_RECEIVED: RESPONSE_RECEIVED_PARAM }: 
SearchParamsKeysType =
-  SearchParamsKeys;
+const {
+  DAG_DISPLAY_NAME_PATTERN,
+  OFFSET: OFFSET_PARAM,
+  RESPONSE_RECEIVED: RESPONSE_RECEIVED_PARAM,
+  TASK_ID_PATTERN,
+}: SearchParamsKeysType = SearchParamsKeys;
 
 const taskInstanceColumns = ({
   dagId,
@@ -122,71 +127,46 @@ export const HITLTaskInstances = () => {
   const [sort] = sorting;
   const responseReceived = searchParams.get(RESPONSE_RECEIVED_PARAM);
 
+  const dagIdPattern = searchParams.get(DAG_DISPLAY_NAME_PATTERN) ?? undefined;
+  const taskIdPattern = searchParams.get(TASK_ID_PATTERN) ?? undefined;
+  const filterResponseReceived = searchParams.get(RESPONSE_RECEIVED_PARAM) ?? 
undefined;
+
+  // Use the filter value if available, otherwise fall back to the old 
responseReceived param
+  const effectiveResponseReceived = filterResponseReceived ?? responseReceived;
+
   const { data, error, isLoading } = useTaskInstanceServiceGetHitlDetails({
     dagId: dagId ?? "~",
+    dagIdPattern,
     dagRunId: runId ?? "~",
     limit: pagination.pageSize,
     offset: pagination.pageIndex * pagination.pageSize,
     orderBy: sort ? [`${sort.desc ? "-" : ""}${sort.id}`] : [],
-    responseReceived: Boolean(responseReceived) ? responseReceived === "true" 
: undefined,
-    state: responseReceived === "false" ? ["deferred"] : undefined,
+    responseReceived:
+      Boolean(effectiveResponseReceived) && effectiveResponseReceived !== "all"
+        ? effectiveResponseReceived === "true"
+        : undefined,
+    state: effectiveResponseReceived === "false" ? ["deferred"] : undefined,
     taskId,
+    taskIdPattern,
   });
 
-  const enabledOptions = createListCollection({
-    items: [
-      { label: translate("filters.response.all"), value: "all" },
-      { label: translate("filters.response.pending"), value: "false" },
-      { label: translate("filters.response.received"), value: "true" },
-    ],
-  });
-
-  const handleResponseChange = useCallback(
-    ({ value }: { value: Array<string> }) => {
-      const [val] = value;
-
-      if (val === undefined || val === "all") {
-        searchParams.delete(RESPONSE_RECEIVED_PARAM);
-      } else {
-        searchParams.set(RESPONSE_RECEIVED_PARAM, val);
-      }
-      setTableURLState({
-        pagination: { ...pagination, pageIndex: 0 },
-        sorting,
-      });
-      searchParams.delete(OFFSET_PARAM);
-      setSearchParams(searchParams);
-    },
-    [searchParams, setSearchParams, pagination, sorting, setTableURLState],
-  );
+  const handleResponseChange = useCallback(() => {
+    setTableURLState({
+      pagination: { ...pagination, pageIndex: 0 },
+      sorting,
+    });
+    searchParams.delete(OFFSET_PARAM);
+    setSearchParams(searchParams);
+  }, [pagination, searchParams, setSearchParams, setTableURLState, sorting]);
 
   return (
-    <Box>
+    <VStack align="start">
       {!Boolean(dagId) && !Boolean(runId) && !Boolean(taskId) ? (
         <Heading size="md">
           {data?.total_entries} {translate("requiredAction", { count: 
data?.total_entries })}
         </Heading>
       ) : undefined}
-      <Box mt={3}>
-        <Select.Root
-          collection={enabledOptions}
-          maxW="250px"
-          onValueChange={handleResponseChange}
-          value={[responseReceived ?? "all"]}
-        >
-          <Select.Label 
fontSize="xs">{translate("requiredActionState")}</Select.Label>
-          <Select.Trigger isActive={Boolean(responseReceived)}>
-            <Select.ValueText />
-          </Select.Trigger>
-          <Select.Content>
-            {enabledOptions.items.map((option) => (
-              <Select.Item item={option} key={option.label}>
-                {option.label}
-              </Select.Item>
-            ))}
-          </Select.Content>
-        </Select.Root>
-      </Box>
+      <HITLFilters onResponseChange={handleResponseChange} />
       <DataTable
         columns={taskInstanceColumns({
           dagId,
@@ -202,6 +182,6 @@ export const HITLTaskInstances = () => {
         onStateChange={setTableURLState}
         total={data?.total_entries}
       />
-    </Box>
+    </VStack>
   );
 };
diff --git a/airflow-core/src/airflow/ui/src/utils/useFiltersHandler.ts 
b/airflow-core/src/airflow/ui/src/utils/useFiltersHandler.ts
index 241a6e9287a..ec1bcf710f2 100644
--- a/airflow-core/src/airflow/ui/src/utils/useFiltersHandler.ts
+++ b/airflow-core/src/airflow/ui/src/utils/useFiltersHandler.ts
@@ -35,6 +35,7 @@ export type FilterableSearchParamsKeys =
   | SearchParamsKeys.LOGICAL_DATE_GTE
   | SearchParamsKeys.LOGICAL_DATE_LTE
   | SearchParamsKeys.MAP_INDEX
+  | SearchParamsKeys.RESPONSE_RECEIVED
   | SearchParamsKeys.RUN_AFTER_GTE
   | SearchParamsKeys.RUN_AFTER_LTE
   | SearchParamsKeys.RUN_ID

Reply via email to