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 1e9e271b3c899..637b1752497c2 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
@@ -7,6 +7,7 @@
"Providers": "Providers",
"Variables": "Variables"
},
+ "allOperators": "All Operators",
"asset_one": "Asset",
"asset_other": "Assets",
"assetEvent_one": "Asset Event",
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/tasks.json b/airflow-core/src/airflow/ui/public/i18n/locales/en/tasks.json
new file mode 100644
index 0000000000000..113b0f01070ac
--- /dev/null
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/tasks.json
@@ -0,0 +1,10 @@
+{
+ "mapped": "Mapped",
+ "notMapped": "Not mapped",
+ "retries": "Retries",
+ "searchTasks": "Search tasks",
+ "selectMapped": "Select mapped",
+ "selectOperator": "Select operators",
+ "selectRetryValues": "Select retry values",
+ "selectTriggerRules": "Select trigger rules"
+}
diff --git a/airflow-core/src/airflow/ui/src/constants/searchParams.ts b/airflow-core/src/airflow/ui/src/constants/searchParams.ts
index 05165124950e6..cc68b52843d54 100644
--- a/airflow-core/src/airflow/ui/src/constants/searchParams.ts
+++ b/airflow-core/src/airflow/ui/src/constants/searchParams.ts
@@ -35,12 +35,15 @@ export enum SearchParamsKeys {
LOGICAL_DATE_GTE = "logical_date_gte",
LOGICAL_DATE_LTE = "logical_date_lte",
MAP_INDEX = "map_index",
+ MAPPED = "mapped",
NAME_PATTERN = "name_pattern",
OFFSET = "offset",
+ OPERATOR = "operator",
OWNERS = "owners",
PAUSED = "paused",
POOL = "pool",
RESPONSE_RECEIVED = "response_received",
+ RETRIES = "retries",
RUN_AFTER_GTE = "run_after_gte",
RUN_AFTER_LTE = "run_after_lte",
RUN_ID = "run_id",
@@ -54,6 +57,7 @@ export enum SearchParamsKeys {
TAGS_MATCH_MODE = "tags_match_mode",
TASK_ID = "task_id",
TASK_ID_PATTERN = "task_id_pattern",
+ TRIGGER_RULE = "trigger_rule",
TRIGGERING_USER_NAME_PATTERN = "triggering_user_name_pattern",
TRY_NUMBER = "try_number",
USER = "user",
diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Tasks/TaskFilters/AttrSelectFilter.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Tasks/TaskFilters/AttrSelectFilter.tsx
new file mode 100644
index 0000000000000..50ef4d4201066
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Tasks/TaskFilters/AttrSelectFilter.tsx
@@ -0,0 +1,60 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { type CollectionItem, createListCollection, type SelectValueChangeDetails } from "@chakra-ui/react";
+
+import { Select } from "src/components/ui";
+
+type Props = {
+ readonly handleSelect: (value: CollectionItem) => void;
+ readonly placeholderText: string;
+ readonly selectedValue: string | undefined;
+ readonly values: Array<{ key: string; label: string }> | undefined;
+};
+
+export const AttrSelectFilter = ({ handleSelect, placeholderText, selectedValue, values }: Props) => {
+ const thingCollection = createListCollection({ items: values ?? [] });
+ const handleValueChange = (details: SelectValueChangeDetails) => {
+ if (Array.isArray(details.value)) {
+ handleSelect(details.value[0]);
+ }
+ };
+ const selectedDisplay = values?.find((item) => item.key === selectedValue);
+
+ return (
+
+
+
+ {() => selectedDisplay?.label}
+
+
+
+ {thingCollection.items.map((option) => (
+
+ {option.label}
+
+ ))}
+
+
+ );
+};
diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Tasks/TaskFilters/AttrSelectFilterMulti.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Tasks/TaskFilters/AttrSelectFilterMulti.tsx
new file mode 100644
index 0000000000000..5a74c239dcb0a
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Tasks/TaskFilters/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) => void;
+ readonly placeholderText: string;
+ readonly selectedValues: Array | undefined;
+ readonly values: Array | 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 (
+
+
+
+ {() => displayValue}
+
+
+
+ {thingCollection.items.map((option) => (
+
+ {option}
+
+ ))}
+
+
+ );
+};
diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Tasks/TaskFilters/TaskFilters.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Tasks/TaskFilters/TaskFilters.tsx
new file mode 100644
index 0000000000000..3abd8b9e238c9
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Tasks/TaskFilters/TaskFilters.tsx
@@ -0,0 +1,136 @@
+/*!
+ * 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 { useTranslation } from "react-i18next";
+import { useSearchParams } from "react-router-dom";
+
+import type { TaskCollectionResponse } from "openapi/requests";
+import { SearchBar } from "src/components/SearchBar.tsx";
+import { ResetButton } from "src/components/ui";
+import { SearchParamsKeys } from "src/constants/searchParams.ts";
+import { AttrSelectFilter } from "src/pages/Dag/Tasks/TaskFilters/AttrSelectFilter.tsx";
+import { AttrSelectFilterMulti } from "src/pages/Dag/Tasks/TaskFilters/AttrSelectFilterMulti.tsx";
+
+export const TaskFilters = ({ tasksData }: { readonly tasksData: TaskCollectionResponse | undefined }) => {
+ const { MAPPED, NAME_PATTERN, OPERATOR, RETRIES, TRIGGER_RULE } = SearchParamsKeys;
+ const { t: translate } = useTranslation("tasks");
+ const [searchParams, setSearchParams] = useSearchParams();
+ const selectedOperators = searchParams.getAll(OPERATOR);
+ const selectedTriggerRules = searchParams.getAll(TRIGGER_RULE);
+ const selectedRetries = searchParams.getAll(RETRIES);
+ const selectedMapped = searchParams.get(MAPPED) ?? undefined;
+
+ const handleSelectedOperators = (value: Array | undefined) => {
+ searchParams.delete(OPERATOR);
+ value?.forEach((x) => searchParams.append(OPERATOR, x));
+ setSearchParams(searchParams);
+ };
+ const handleSelectedRetries = (value: Array | undefined) => {
+ searchParams.delete(RETRIES);
+ value?.forEach((x) => searchParams.append(RETRIES, x));
+ setSearchParams(searchParams);
+ };
+ const handleSelectedTriggerRules = (value: Array | undefined) => {
+ searchParams.delete(TRIGGER_RULE);
+ value?.forEach((x) => searchParams.append(TRIGGER_RULE, x));
+ setSearchParams(searchParams);
+ };
+ const handleSelectedMapped = (value: string | undefined) => {
+ searchParams.delete(MAPPED);
+ if (value !== undefined) {
+ searchParams.set(MAPPED, value);
+ }
+ setSearchParams(searchParams);
+ };
+
+ const onClearFilters = () => {
+ setSearchParams();
+ };
+
+ const allOperatorNames: Array = [
+ ...new Set(tasksData?.tasks.map((task) => task.operator_name).filter((item) => item !== null) ?? []),
+ ];
+ const allTriggerRules: Array = [
+ ...new Set(tasksData?.tasks.map((task) => task.trigger_rule).filter((item) => item !== null) ?? []),
+ ];
+ const allRetryValues: Array = [
+ ...new Set(
+ tasksData?.tasks.map((task) => task.retries?.toString()).filter((item) => item !== undefined) ?? [],
+ ),
+ ];
+ const allMappedValues = [
+ { key: "true", label: translate("mapped") },
+ { key: "false", label: translate("notMapped") },
+ ];
+ const taskNamePattern = searchParams.get(NAME_PATTERN) ?? "";
+ const handleSearchChange = (value: string) => {
+ if (value) {
+ searchParams.set(NAME_PATTERN, value);
+ } else {
+ searchParams.delete(NAME_PATTERN);
+ }
+ setSearchParams(searchParams);
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Tasks/Tasks.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Tasks/Tasks.tsx
index 063790aba478f..e378b5c63ae3c 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Tasks/Tasks.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Tasks/Tasks.tsx
@@ -16,15 +16,17 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { Heading, Skeleton, Box } from "@chakra-ui/react";
+import { Skeleton, Box } from "@chakra-ui/react";
import { useTranslation } from "react-i18next";
-import { useParams } from "react-router-dom";
+import { useParams, useSearchParams } from "react-router-dom";
import { useTaskServiceGetTasks } from "openapi/queries";
import type { TaskResponse } from "openapi/requests/types.gen";
import { DataTable } from "src/components/DataTable";
import type { CardDef } from "src/components/DataTable/types";
import { ErrorAlert } from "src/components/ErrorAlert";
+import { SearchParamsKeys } from "src/constants/searchParams.ts";
+import { TaskFilters } from "src/pages/Dag/Tasks/TaskFilters/TaskFilters.tsx";
import { TaskCard } from "./TaskCard";
@@ -36,8 +38,16 @@ const cardDef = (dagId: string): CardDef => ({
});
export const Tasks = () => {
- const { t: translate } = useTranslation();
const { dagId = "" } = useParams();
+ const { MAPPED, NAME_PATTERN, OPERATOR, RETRIES, TRIGGER_RULE } = SearchParamsKeys;
+ const { t: translate } = useTranslation();
+ const [searchParams] = useSearchParams();
+ const selectedOperators = searchParams.getAll(OPERATOR);
+ const selectedTriggerRules = searchParams.getAll(TRIGGER_RULE);
+ const selectedRetries = searchParams.getAll(RETRIES);
+ const selectedMapped = searchParams.get(MAPPED) ?? undefined;
+ const namePattern = searchParams.get(NAME_PATTERN) ?? undefined;
+
const {
data,
error: tasksError,
@@ -47,16 +57,46 @@ export const Tasks = () => {
dagId,
});
+ const filterTasks = ({
+ mapped,
+ operatorNames,
+ retryValues,
+ tasks,
+ triggerRuleNames,
+ }: {
+ mapped: string | undefined;
+ operatorNames: Array;
+ retryValues: Array;
+ tasks: Array;
+ triggerRuleNames: Array;
+ }) =>
+ tasks.filter(
+ (task) =>
+ (operatorNames.length === 0 || operatorNames.includes(task.operator_name as string)) &&
+ (triggerRuleNames.length === 0 || triggerRuleNames.includes(task.trigger_rule as string)) &&
+ (retryValues.length === 0 || retryValues.includes(task.retries?.toString() as string)) &&
+ (mapped === undefined || task.is_mapped?.toString() === mapped) &&
+ (namePattern === undefined || task.task_display_name?.toString().includes(namePattern)),
+ );
+
+ const filteredTasks = filterTasks({
+ mapped: selectedMapped,
+ operatorNames: selectedOperators,
+ retryValues: selectedRetries,
+ tasks: data ? data.tasks : [],
+ triggerRuleNames: selectedTriggerRules,
+ });
+
return (
-
- {translate("task", { count: data?.total_entries ?? 0 })}
-
+
+
+