bbovenzi commented on code in PR #56920:
URL: https://github.com/apache/airflow/pull/56920#discussion_r2448809354


##########
airflow-core/src/airflow/ui/src/pages/TaskInstances/AttrSelectFilterMulti.tsx:
##########
@@ -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 { type CollectionItem, createListCollection } from "@chakra-ui/react";
+import type { SelectValueChangeDetails } from "@chakra-ui/react";
+
+import { Select } from "src/components/ui";
+
+type Props = {
+  readonly displayPrefix: string | undefined;
+  readonly handleSelect: (values: Array<CollectionItem>) => void;
+  readonly placeholderText: string;
+  readonly selectedValues: Array<string> | undefined;
+  readonly values: Array<string> | undefined;
+};
+
+export const AttrSelectFilterMulti = ({
+  displayPrefix,
+  handleSelect,
+  placeholderText,
+  selectedValues,
+  values,
+}: Props) => {
+  const thingCollection = createListCollection({ items: values ?? [] });
+
+  const handleValueChange = (details: SelectValueChangeDetails) => {
+    if (Array.isArray(details.value)) {
+      handleSelect(details.value);
+    }
+  };
+  let displayValue = selectedValues?.join(", ") ?? undefined;
+
+  if (displayValue !== undefined && displayPrefix !== undefined) {
+    displayValue = `${displayPrefix}: ${displayValue}`;
+  }
+
+  // debugger;

Review Comment:
   ```suggestion
   ```



##########
airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstancesFilter.tsx:
##########
@@ -16,39 +16,122 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { HStack, type SelectValueChangeDetails } from "@chakra-ui/react";
-import { useCallback } from "react";
+import { HStack, VStack, type SelectValueChangeDetails, Box } from 
"@chakra-ui/react";
+import { useCallback, useMemo } from "react";
 import { useTranslation } from "react-i18next";
 import { useSearchParams, useParams } from "react-router-dom";
 
-import type { TaskInstanceState } from "openapi/requests/types.gen";
+import type { TaskInstanceCollectionResponse } from "openapi/requests";
 import { useTableURLState } from "src/components/DataTable/useTableUrlState";
+import { FilterBar, type FilterValue } from "src/components/FilterBar";
 import { SearchBar } from "src/components/SearchBar";
-import { StateBadge } from "src/components/StateBadge";
-import { Select } from "src/components/ui";
+import { ResetButton } from "src/components/ui";
 import { SearchParamsKeys, type SearchParamsKeysType } from 
"src/constants/searchParams";
-import { taskInstanceStateOptions } from "src/constants/stateOptions";
+import { useFiltersHandler, type FilterableSearchParamsKeys } from "src/utils";
+
+import { AttrSelectFilterMulti } from "./AttrSelectFilterMulti";
+import { StateFilter } from "./StateFilter";
 
 const {
   DAG_ID_PATTERN: DAG_ID_PATTERN_PARAM,
+  DAG_VERSION: DAG_VERSION_PARAM,
+  DURATION_GTE: DURATION_GTE_PARAM,
+  DURATION_LTE: DURATION_LTE_PARAM,
+  END_DATE: END_DATE_PARAM,
+  LOGICAL_DATE_GTE: LOGICAL_DATE_GTE_PARAM,
+  LOGICAL_DATE_LTE: LOGICAL_DATE_LTE_PARAM,
+  MAP_INDEX: MAP_INDEX_PARAM,
   NAME_PATTERN: NAME_PATTERN_PARAM,
+  OPERATOR: OPERATOR_PARAM,
+  POOL: POOL_PARAM,
+  QUEUE: QUEUE_PARAM,
+  RUN_ID: RUN_ID_PARAM,
+  START_DATE: START_DATE_PARAM,
   STATE: STATE_PARAM,
+  TRY_NUMBER: TRY_NUMBER_PARAM,
 }: SearchParamsKeysType = SearchParamsKeys;
 
+type Props = {
+  readonly instances?: TaskInstanceCollectionResponse | undefined;
+  readonly setTaskDisplayNamePattern: 
React.Dispatch<React.SetStateAction<string | undefined>>;
+  readonly taskDisplayNamePattern: string | undefined;
+};
+
 export const TaskInstancesFilter = ({
+  instances,
   setTaskDisplayNamePattern,
   taskDisplayNamePattern,
-}: {
-  readonly setTaskDisplayNamePattern: 
React.Dispatch<React.SetStateAction<string | undefined>>;
-  readonly taskDisplayNamePattern: string | undefined;
-}) => {
+}: Props) => {
   const { dagId, runId } = useParams();
+  const paramKeys = useMemo((): Array<FilterableSearchParamsKeys> => {
+    const keys: Array<FilterableSearchParamsKeys> = [
+      LOGICAL_DATE_GTE_PARAM as FilterableSearchParamsKeys,
+      LOGICAL_DATE_LTE_PARAM as FilterableSearchParamsKeys,
+      START_DATE_PARAM as FilterableSearchParamsKeys,
+      END_DATE_PARAM as FilterableSearchParamsKeys,
+      DURATION_GTE_PARAM as FilterableSearchParamsKeys,
+      DURATION_LTE_PARAM as FilterableSearchParamsKeys,
+      TRY_NUMBER_PARAM as FilterableSearchParamsKeys,
+      MAP_INDEX_PARAM as FilterableSearchParamsKeys,
+      DAG_VERSION_PARAM as FilterableSearchParamsKeys,
+    ];
+
+    if (runId === undefined) {
+      keys.splice(1, 0, RUN_ID_PARAM as FilterableSearchParamsKeys);
+    }
+
+    return keys;
+  }, [runId]);
+
   const [searchParams, setSearchParams] = useSearchParams();
   const { setTableURLState, tableURLState } = useTableURLState();
   const { pagination, sorting } = tableURLState;
   const { t: translate } = useTranslation();
 
+  const { filterConfigs, handleFiltersChange } = useFiltersHandler(paramKeys);
+
+  const uniq = (xs: Array<string | null | undefined>) => [
+    ...new Set(xs.filter((x): x is string => x !== null && x !== undefined && 
x !== "")),
+  ];

Review Comment:
   This should have a comment saying what the function does and be declared 
outside of the component above line 60



##########
airflow-core/src/airflow/ui/src/pages/TaskInstances/AttrSelectFilterMulti.tsx:
##########
@@ -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 { type CollectionItem, createListCollection } from "@chakra-ui/react";
+import type { SelectValueChangeDetails } from "@chakra-ui/react";
+
+import { Select } from "src/components/ui";
+
+type Props = {
+  readonly displayPrefix: string | undefined;
+  readonly handleSelect: (values: Array<CollectionItem>) => void;
+  readonly placeholderText: string;
+  readonly selectedValues: Array<string> | undefined;
+  readonly values: Array<string> | undefined;
+};
+
+export const AttrSelectFilterMulti = ({
+  displayPrefix,
+  handleSelect,
+  placeholderText,
+  selectedValues,
+  values,
+}: Props) => {
+  const thingCollection = createListCollection({ items: values ?? [] });

Review Comment:
   Let's avoid "thing". Even `valueCollection` is a more descriptive name



##########
airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstancesFilter.tsx:
##########
@@ -16,39 +16,122 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { HStack, type SelectValueChangeDetails } from "@chakra-ui/react";
-import { useCallback } from "react";
+import { HStack, VStack, type SelectValueChangeDetails, Box } from 
"@chakra-ui/react";
+import { useCallback, useMemo } from "react";
 import { useTranslation } from "react-i18next";
 import { useSearchParams, useParams } from "react-router-dom";
 
-import type { TaskInstanceState } from "openapi/requests/types.gen";
+import type { TaskInstanceCollectionResponse } from "openapi/requests";
 import { useTableURLState } from "src/components/DataTable/useTableUrlState";
+import { FilterBar, type FilterValue } from "src/components/FilterBar";
 import { SearchBar } from "src/components/SearchBar";
-import { StateBadge } from "src/components/StateBadge";
-import { Select } from "src/components/ui";
+import { ResetButton } from "src/components/ui";
 import { SearchParamsKeys, type SearchParamsKeysType } from 
"src/constants/searchParams";
-import { taskInstanceStateOptions } from "src/constants/stateOptions";
+import { useFiltersHandler, type FilterableSearchParamsKeys } from "src/utils";
+
+import { AttrSelectFilterMulti } from "./AttrSelectFilterMulti";
+import { StateFilter } from "./StateFilter";
 
 const {
   DAG_ID_PATTERN: DAG_ID_PATTERN_PARAM,
+  DAG_VERSION: DAG_VERSION_PARAM,
+  DURATION_GTE: DURATION_GTE_PARAM,
+  DURATION_LTE: DURATION_LTE_PARAM,
+  END_DATE: END_DATE_PARAM,
+  LOGICAL_DATE_GTE: LOGICAL_DATE_GTE_PARAM,
+  LOGICAL_DATE_LTE: LOGICAL_DATE_LTE_PARAM,
+  MAP_INDEX: MAP_INDEX_PARAM,
   NAME_PATTERN: NAME_PATTERN_PARAM,
+  OPERATOR: OPERATOR_PARAM,
+  POOL: POOL_PARAM,
+  QUEUE: QUEUE_PARAM,
+  RUN_ID: RUN_ID_PARAM,
+  START_DATE: START_DATE_PARAM,
   STATE: STATE_PARAM,
+  TRY_NUMBER: TRY_NUMBER_PARAM,
 }: SearchParamsKeysType = SearchParamsKeys;
 
+type Props = {
+  readonly instances?: TaskInstanceCollectionResponse | undefined;
+  readonly setTaskDisplayNamePattern: 
React.Dispatch<React.SetStateAction<string | undefined>>;
+  readonly taskDisplayNamePattern: string | undefined;
+};
+
 export const TaskInstancesFilter = ({
+  instances,
   setTaskDisplayNamePattern,
   taskDisplayNamePattern,
-}: {
-  readonly setTaskDisplayNamePattern: 
React.Dispatch<React.SetStateAction<string | undefined>>;
-  readonly taskDisplayNamePattern: string | undefined;
-}) => {
+}: Props) => {
   const { dagId, runId } = useParams();
+  const paramKeys = useMemo((): Array<FilterableSearchParamsKeys> => {
+    const keys: Array<FilterableSearchParamsKeys> = [
+      LOGICAL_DATE_GTE_PARAM as FilterableSearchParamsKeys,
+      LOGICAL_DATE_LTE_PARAM as FilterableSearchParamsKeys,
+      START_DATE_PARAM as FilterableSearchParamsKeys,
+      END_DATE_PARAM as FilterableSearchParamsKeys,
+      DURATION_GTE_PARAM as FilterableSearchParamsKeys,
+      DURATION_LTE_PARAM as FilterableSearchParamsKeys,
+      TRY_NUMBER_PARAM as FilterableSearchParamsKeys,
+      MAP_INDEX_PARAM as FilterableSearchParamsKeys,
+      DAG_VERSION_PARAM as FilterableSearchParamsKeys,
+    ];
+
+    if (runId === undefined) {
+      keys.splice(1, 0, RUN_ID_PARAM as FilterableSearchParamsKeys);
+    }
+
+    return keys;
+  }, [runId]);
+
   const [searchParams, setSearchParams] = useSearchParams();
   const { setTableURLState, tableURLState } = useTableURLState();
   const { pagination, sorting } = tableURLState;
   const { t: translate } = useTranslation();
 
+  const { filterConfigs, handleFiltersChange } = useFiltersHandler(paramKeys);
+
+  const uniq = (xs: Array<string | null | undefined>) => [
+    ...new Set(xs.filter((x): x is string => x !== null && x !== undefined && 
x !== "")),
+  ];
+
+  const resetPagination = useCallback(() => {
+    setTableURLState({
+      pagination: { ...pagination, pageIndex: 0 },
+      sorting,
+    });
+  }, [pagination, sorting, setTableURLState]);
+
+  const setMultiParam = useCallback(
+    (key: string, values: Array<string>) => {
+      resetPagination();
+      setSearchParams((prev) => {
+        const next = new URLSearchParams(prev);
+
+        next.delete(key);
+        values.forEach((val) => next.append(key, val));
+
+        return next;
+      });
+    },
+    [resetPagination, setSearchParams],
+  );
+
+  const allOperatorNames: Array<string> = uniq(
+    instances?.task_instances.map((ti) => ti.operator_name as string | null | 
undefined) ?? [],
+  );
+  const allQueueValues: Array<string> = uniq(
+    instances?.task_instances.map((ti) => ti.queue as string | null | 
undefined) ?? [],
+  );
+  const allPoolValues: Array<string> = uniq(
+    instances?.task_instances.map((ti) => ti.pool as string | null | 
undefined) ?? [],
+  );

Review Comment:
   We shouldn't try to calculate these values on the frontend. There can be an 
operator on another page of results which wouldn't show up.
   Either, we need a specific API endpoint to generate these values for us or 
we should let it simply be a text search. 



##########
airflow-core/src/airflow/ui/src/pages/TaskInstances/AttrSelectFilterMulti.tsx:
##########
@@ -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 { type CollectionItem, createListCollection } from "@chakra-ui/react";
+import type { SelectValueChangeDetails } from "@chakra-ui/react";
+
+import { Select } from "src/components/ui";
+
+type Props = {
+  readonly displayPrefix: string | undefined;
+  readonly handleSelect: (values: Array<CollectionItem>) => void;
+  readonly placeholderText: string;
+  readonly selectedValues: Array<string> | undefined;
+  readonly values: Array<string> | undefined;
+};
+
+export const AttrSelectFilterMulti = ({
+  displayPrefix,
+  handleSelect,
+  placeholderText,
+  selectedValues,
+  values,
+}: Props) => {
+  const thingCollection = createListCollection({ items: values ?? [] });
+
+  const handleValueChange = (details: SelectValueChangeDetails) => {
+    if (Array.isArray(details.value)) {
+      handleSelect(details.value);
+    }
+  };
+  let displayValue = selectedValues?.join(", ") ?? undefined;
+
+  if (displayValue !== undefined && displayPrefix !== undefined) {
+    displayValue = `${displayPrefix}: ${displayValue}`;
+  }
+
+  // debugger;
+  return (
+    <Select.Root
+      collection={thingCollection}
+      multiple
+      onValueChange={handleValueChange}
+      value={selectedValues}
+      width="200px"
+    >
+      <Select.Trigger colorPalette="brand">
+        <Select.ValueText placeholder={placeholderText} width="200px">
+          {() => displayValue}

Review Comment:
   Why is this a function?



##########
airflow-core/src/airflow/ui/src/pages/TaskInstances/StateFilter.tsx:
##########
@@ -0,0 +1,88 @@
+/*!
+ * 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 { HStack, type SelectValueChangeDetails } from "@chakra-ui/react";
+
+import type { TaskInstanceState } from "openapi/requests/types.gen";
+import { StateBadge } from "src/components/StateBadge";
+import { Select } from "src/components/ui";
+import { taskInstanceStateOptions } from "src/constants/stateOptions";
+
+type Props<V extends string> = {
+  readonly maxW?: string;
+  readonly onChange: (details: SelectValueChangeDetails<V>) => void;
+  readonly translate: (key: string) => string;
+  readonly value: Array<V>; // e.g. (TaskInstanceState | "all" | "none")[]
+};
+
+export const StateFilter = <V extends string>({ maxW = "450px", onChange, 
translate, value }: Props<V>) => {

Review Comment:
   Why do we need a Generic type here?



##########
airflow-core/src/airflow/ui/public/i18n/locales/en/taskInstances.json:
##########


Review Comment:
   Let's not create a new translation file. Instead, let's add it to an 
existing one.



##########
airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstancesFilter.tsx:
##########
@@ -101,66 +191,106 @@ export const TaskInstancesFilter = ({
     [pagination, searchParams, setSearchParams, setTableURLState, sorting],
   );
 
+  const onClearFilters = useCallback(() => {
+    resetPagination();
+    setSearchParams((prev) => {
+      const next = new URLSearchParams(prev);
+
+      next.delete(STATE_PARAM);
+      next.delete(OPERATOR_PARAM);
+      next.delete(QUEUE_PARAM);
+      next.delete(POOL_PARAM);
+
+      return next;
+    });
+  }, [setSearchParams, resetPagination]);
+
+  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 values;
+  }, [searchParams, filterConfigs]);
+
+  const taskFilterCount =
+    (searchParams.getAll(STATE_PARAM).length > 0 ? 1 : 0) +
+    (searchParams.getAll(OPERATOR_PARAM).length > 0 ? 1 : 0) +
+    (searchParams.getAll(QUEUE_PARAM).length > 0 ? 1 : 0) +
+    (searchParams.getAll(POOL_PARAM).length > 0 ? 1 : 0);
+
   return (
-    <HStack paddingY="4px">
-      {dagId === undefined && (
+    <VStack align="start" justifyContent="space-between">
+      <HStack alignItems="start" paddingY="4px" minWidth="100%">
+        {dagId === undefined && (
+          <SearchBar
+            buttonProps={{ disabled: true }}
+            defaultValue={filteredDagIdPattern ?? ""}
+            hideAdvanced
+            hotkeyDisabled={true}
+            onChange={handleDagIdPatternChange}
+            placeHolder={translate("dags:search.dags")}
+          />
+        )}
         <SearchBar
           buttonProps={{ disabled: true }}
-          defaultValue={filteredDagIdPattern ?? ""}
+          defaultValue={taskDisplayNamePattern ?? ""}
           hideAdvanced
-          hotkeyDisabled={true}
-          onChange={handleDagIdPatternChange}
-          placeHolder={translate("dags:search.dags")}
+          hotkeyDisabled={Boolean(runId)}
+          onChange={handleSearchChange}
+          placeHolder={translate("dags:search.tasks")}
+        />
+        <StateFilter
+          onChange={handleStateChange}
+          translate={translate}
+          value={hasFilteredState ? filteredState : ["all"]}
+        />
+      </HStack>
+      <HStack>
+        <AttrSelectFilterMulti
+          displayPrefix={undefined}
+          handleSelect={handleSelectedOperators}
+          placeholderText={translate("selectOperator")}
+          selectedValues={selectedOperators}
+          values={allOperatorNames}
+        />
+        <AttrSelectFilterMulti
+          displayPrefix={undefined}
+          handleSelect={handleSelectedQueues}
+          placeholderText={translate("selectQueues")}
+          selectedValues={selectedQueues}
+          values={allQueueValues}
+        />
+        <AttrSelectFilterMulti
+          displayPrefix={undefined}
+          handleSelect={handleSelectedPools}
+          placeholderText={translate("selectPools")}
+          selectedValues={selectedPools}
+          values={allPoolValues}
+        />
+        <Box>
+          <ResetButton filterCount={taskFilterCount} 
onClearFilters={onClearFilters} />
+        </Box>
+      </HStack>
+      <VStack alignItems="flex-start" gap={1}>
+        <FilterBar
+          configs={filterConfigs}
+          initialValues={initialValues}
+          onFiltersChange={handleFiltersChange}
         />
-      )}
-      <SearchBar
-        buttonProps={{ disabled: true }}
-        defaultValue={taskDisplayNamePattern ?? ""}
-        hideAdvanced
-        hotkeyDisabled={Boolean(runId)}
-        onChange={handleSearchChange}
-        placeHolder={translate("dags:search.tasks")}
-      />
-      <Select.Root
-        collection={taskInstanceStateOptions}
-        maxW="450px"
-        multiple
-        onValueChange={handleStateChange}
-        value={hasFilteredState ? filteredState : ["all"]}
-      >
-        <Select.Trigger
-          {...(hasFilteredState ? { clearable: true } : {})}
-          colorPalette="brand"
-          isActive={Boolean(filteredState)}
-        >
-          <Select.ValueText>
-            {() =>
-              hasFilteredState ? (
-                <HStack flexWrap="wrap" fontSize="sm" gap="4px" paddingY="8px">
-                  {filteredState.map((state) => (
-                    <StateBadge key={state} state={state as TaskInstanceState}>
-                      {translate(`common:states.${state}`)}
-                    </StateBadge>
-                  ))}
-                </HStack>
-              ) : (
-                translate("dags:filters.allStates")
-              )
-            }
-          </Select.ValueText>
-        </Select.Trigger>
-        <Select.Content>
-          {taskInstanceStateOptions.items.map((option) => (
-            <Select.Item item={option} key={option.label}>
-              {option.value === "all" ? (
-                translate(option.label)
-              ) : (
-                <StateBadge state={option.value as 
TaskInstanceState}>{translate(option.label)}</StateBadge>
-              )}
-            </Select.Item>
-          ))}
-        </Select.Content>
-      </Select.Root>
-    </HStack>
+      </VStack>

Review Comment:
   We want to move all of the filters into FilterBar. Then they don't take up 
so much room when they're not being used. 



##########
airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstancesFilter.tsx:
##########
@@ -101,66 +191,106 @@ export const TaskInstancesFilter = ({
     [pagination, searchParams, setSearchParams, setTableURLState, sorting],
   );
 
+  const onClearFilters = useCallback(() => {
+    resetPagination();
+    setSearchParams((prev) => {
+      const next = new URLSearchParams(prev);
+
+      next.delete(STATE_PARAM);
+      next.delete(OPERATOR_PARAM);
+      next.delete(QUEUE_PARAM);
+      next.delete(POOL_PARAM);
+
+      return next;
+    });
+  }, [setSearchParams, resetPagination]);
+
+  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 values;
+  }, [searchParams, filterConfigs]);
+
+  const taskFilterCount =
+    (searchParams.getAll(STATE_PARAM).length > 0 ? 1 : 0) +
+    (searchParams.getAll(OPERATOR_PARAM).length > 0 ? 1 : 0) +
+    (searchParams.getAll(QUEUE_PARAM).length > 0 ? 1 : 0) +
+    (searchParams.getAll(POOL_PARAM).length > 0 ? 1 : 0);
+
   return (
-    <HStack paddingY="4px">
-      {dagId === undefined && (
+    <VStack align="start" justifyContent="space-between">
+      <HStack alignItems="start" paddingY="4px" minWidth="100%">
+        {dagId === undefined && (
+          <SearchBar
+            buttonProps={{ disabled: true }}
+            defaultValue={filteredDagIdPattern ?? ""}
+            hideAdvanced
+            hotkeyDisabled={true}
+            onChange={handleDagIdPatternChange}
+            placeHolder={translate("dags:search.dags")}
+          />
+        )}
         <SearchBar
           buttonProps={{ disabled: true }}
-          defaultValue={filteredDagIdPattern ?? ""}
+          defaultValue={taskDisplayNamePattern ?? ""}
           hideAdvanced
-          hotkeyDisabled={true}
-          onChange={handleDagIdPatternChange}
-          placeHolder={translate("dags:search.dags")}
+          hotkeyDisabled={Boolean(runId)}
+          onChange={handleSearchChange}
+          placeHolder={translate("dags:search.tasks")}
+        />
+        <StateFilter
+          onChange={handleStateChange}
+          translate={translate}

Review Comment:
   We shouldn't pass `translate` down to StateFilter. It can call the hook 
itself.



##########
airflow-core/src/airflow/ui/src/pages/TaskInstances/StateFilter.tsx:
##########
@@ -0,0 +1,88 @@
+/*!
+ * 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 { HStack, type SelectValueChangeDetails } from "@chakra-ui/react";
+
+import type { TaskInstanceState } from "openapi/requests/types.gen";
+import { StateBadge } from "src/components/StateBadge";
+import { Select } from "src/components/ui";
+import { taskInstanceStateOptions } from "src/constants/stateOptions";
+
+type Props<V extends string> = {
+  readonly maxW?: string;
+  readonly onChange: (details: SelectValueChangeDetails<V>) => void;
+  readonly translate: (key: string) => string;
+  readonly value: Array<V>; // e.g. (TaskInstanceState | "all" | "none")[]
+};
+
+export const StateFilter = <V extends string>({ maxW = "450px", onChange, 
translate, value }: Props<V>) => {

Review Comment:
   I also don't think we even need a special component here. Look at 
`filterConfigs.tsx`, we just need to make a filter config to task instance 
states



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to