diff --git a/airflow/ui/src/components/SearchBar.tsx b/airflow/ui/src/components/SearchBar.tsx index 1e3124fa415ae..349b66ce9e2e6 100644 --- a/airflow/ui/src/components/SearchBar.tsx +++ b/airflow/ui/src/components/SearchBar.tsx @@ -29,11 +29,19 @@ type Props = { readonly buttonProps?: ButtonProps; readonly defaultValue: string; readonly groupProps?: InputGroupProps; + readonly hideAdvanced?: boolean; readonly onChange: (value: string) => void; readonly placeHolder: string; }; -export const SearchBar = ({ buttonProps, defaultValue, groupProps, onChange, placeHolder }: Props) => { +export const SearchBar = ({ + buttonProps, + defaultValue, + groupProps, + hideAdvanced = false, + onChange, + placeHolder, +}: Props) => { const handleSearchChange = useDebouncedCallback((val: string) => onChange(val), debounceDelay); const [value, setValue] = useState(defaultValue); @@ -61,9 +69,11 @@ export const SearchBar = ({ buttonProps, defaultValue, groupProps, onChange, pla size="xs" /> ) : undefined} - + {Boolean(hideAdvanced) ? undefined : ( + + )} } startElement={} diff --git a/airflow/ui/src/pages/Run/TaskInstances.tsx b/airflow/ui/src/pages/Run/TaskInstances.tsx index bbfaa35babef4..48c322a7a12a0 100644 --- a/airflow/ui/src/pages/Run/TaskInstances.tsx +++ b/airflow/ui/src/pages/Run/TaskInstances.tsx @@ -16,18 +16,21 @@ * specific language governing permissions and limitations * under the License. */ -import { Box, Link } from "@chakra-ui/react"; +import { Box, Link, createListCollection, HStack, type SelectValueChangeDetails } from "@chakra-ui/react"; import type { ColumnDef } from "@tanstack/react-table"; -import { Link as RouterLink, useParams } from "react-router-dom"; +import { useCallback, useState } from "react"; +import { Link as RouterLink, useParams, useSearchParams } from "react-router-dom"; import { useTaskInstanceServiceGetTaskInstances } from "openapi/queries"; -import type { TaskInstanceResponse } from "openapi/requests/types.gen"; +import type { TaskInstanceResponse, TaskInstanceState } from "openapi/requests/types.gen"; import { DataTable } from "src/components/DataTable"; import { useTableURLState } from "src/components/DataTable/useTableUrlState"; import { ErrorAlert } from "src/components/ErrorAlert"; +import { SearchBar } from "src/components/SearchBar"; import Time from "src/components/Time"; -import { Status } from "src/components/ui"; -import { getDuration } from "src/utils"; +import { Select, Status } from "src/components/ui"; +import { SearchParamsKeys, type SearchParamsKeysType } from "src/constants/searchParams"; +import { capitalize, getDuration } from "src/utils"; import { getTaskInstanceLink } from "src/utils/links"; const columns: Array> = [ @@ -82,12 +85,74 @@ const columns: Array> = [ }, ]; +const stateOptions = createListCollection<{ label: string; value: TaskInstanceState | "all" | "none" }>({ + items: [ + { label: "All States", value: "all" }, + { label: "Scheduled", value: "scheduled" }, + { label: "Queued", value: "queued" }, + { label: "Running", value: "running" }, + { label: "Success", value: "success" }, + { label: "Restarting", value: "restarting" }, + { label: "Failed", value: "failed" }, + { label: "Up For Retry", value: "up_for_retry" }, + { label: "Up For Reschedule", value: "up_for_reschedule" }, + { label: "Upstream failed", value: "upstream_failed" }, + { label: "Skipped", value: "skipped" }, + { label: "Deferred", value: "deferred" }, + { label: "Removed", value: "removed" }, + { label: "No Status", value: "none" }, + ], +}); + +const STATE_PARAM = "state"; + export const TaskInstances = () => { const { dagId = "", runId = "" } = useParams(); + const [searchParams, setSearchParams] = useSearchParams(); const { setTableURLState, tableURLState } = useTableURLState(); const { pagination, sorting } = tableURLState; const [sort] = sorting; const orderBy = sort ? `${sort.desc ? "-" : ""}${sort.id}` : "-start_date"; + const filteredState = searchParams.getAll(STATE_PARAM); + const hasFilteredState = filteredState.length > 0; + const { NAME_PATTERN: NAME_PATTERN_PARAM }: SearchParamsKeysType = SearchParamsKeys; + + const [taskDisplayNamePattern, setTaskDisplayNamePattern] = useState( + searchParams.get(NAME_PATTERN_PARAM) ?? undefined, + ); + + const handleStateChange = useCallback( + ({ value }: SelectValueChangeDetails) => { + const [val, ...rest] = value; + + if ((val === undefined || val === "all") && rest.length === 0) { + searchParams.delete(STATE_PARAM); + } else { + searchParams.delete(STATE_PARAM); + value.filter((state) => state !== "all").map((state) => searchParams.append(STATE_PARAM, state)); + } + setTableURLState({ + pagination: { ...pagination, pageIndex: 0 }, + sorting, + }); + setSearchParams(searchParams); + }, + [pagination, searchParams, setSearchParams, setTableURLState, sorting], + ); + + const handleSearchChange = (value: string) => { + if (value) { + searchParams.set(NAME_PATTERN_PARAM, value); + } else { + searchParams.delete(NAME_PATTERN_PARAM); + } + setTableURLState({ + pagination: { ...pagination, pageIndex: 0 }, + sorting, + }); + setTaskDisplayNamePattern(value); + setSearchParams(searchParams); + }; const { data, error, isFetching, isLoading } = useTaskInstanceServiceGetTaskInstances( { @@ -96,13 +161,64 @@ export const TaskInstances = () => { limit: pagination.pageSize, offset: pagination.pageIndex * pagination.pageSize, orderBy, + state: hasFilteredState ? filteredState : undefined, + taskDisplayNamePattern: Boolean(taskDisplayNamePattern) ? taskDisplayNamePattern : undefined, }, undefined, { enabled: !isNaN(pagination.pageSize) }, ); return ( - + + + + + + {() => + hasFilteredState ? ( + + {filteredState.map((state) => ( + + {state === "none" ? "No Status" : capitalize(state)} + + ))} + + ) : ( + "All States" + ) + } + + + + {stateOptions.items.map((option) => ( + + {option.value === "all" ? ( + option.label + ) : ( + {option.label} + )} + + ))} + + + +