diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/browse.json b/airflow-core/src/airflow/ui/public/i18n/locales/en/browse.json index 4a2ab97354b16..07259fd1e5825 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/en/browse.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/browse.json @@ -10,6 +10,9 @@ "user": "User", "when": "When" }, + "filters": { + "eventType": "Event Type" + }, "title": "Audit Log" }, "xcom": { diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/ResetButton.tsx b/airflow-core/src/airflow/ui/src/components/ui/ResetButton.tsx similarity index 100% rename from airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/ResetButton.tsx rename to airflow-core/src/airflow/ui/src/components/ui/ResetButton.tsx 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 2146daedb7ac0..b7695029c4ea8 100644 --- a/airflow-core/src/airflow/ui/src/components/ui/index.ts +++ b/airflow-core/src/airflow/ui/src/components/ui/index.ts @@ -34,3 +34,4 @@ export * from "./Breadcrumb"; export * from "./Clipboard"; export * from "./Popover"; export * from "./Checkbox"; +export * from "./ResetButton"; diff --git a/airflow-core/src/airflow/ui/src/constants/searchParams.ts b/airflow-core/src/airflow/ui/src/constants/searchParams.ts index d333695251df4..05165124950e6 100644 --- a/airflow-core/src/airflow/ui/src/constants/searchParams.ts +++ b/airflow-core/src/airflow/ui/src/constants/searchParams.ts @@ -17,11 +17,17 @@ * under the License. */ export enum SearchParamsKeys { + AFTER = "after", + BEFORE = "before", DAG_DISPLAY_NAME_PATTERN = "dag_display_name_pattern", + DAG_ID = "dag_id", DAG_ID_PATTERN = "dag_id_pattern", DEPENDENCIES = "dependencies", END_DATE = "end_date", + EVENT_TYPE = "event_type", + EXCLUDED_EVENTS = "excluded_events", FAVORITE = "favorite", + INCLUDED_EVENTS = "included_events", KEY_PATTERN = "key_pattern", LAST_DAG_RUN_STATE = "last_dag_run_state", LIMIT = "limit", @@ -37,6 +43,7 @@ export enum SearchParamsKeys { RESPONSE_RECEIVED = "response_received", RUN_AFTER_GTE = "run_after_gte", RUN_AFTER_LTE = "run_after_lte", + RUN_ID = "run_id", RUN_ID_PATTERN = "run_id_pattern", RUN_TYPE = "run_type", SORT = "sort", @@ -45,9 +52,11 @@ export enum SearchParamsKeys { STATE = "state", TAGS = "tags", TAGS_MATCH_MODE = "tags_match_mode", + TASK_ID = "task_id", TASK_ID_PATTERN = "task_id_pattern", TRIGGERING_USER_NAME_PATTERN = "triggering_user_name_pattern", TRY_NUMBER = "try_number", + USER = "user", VERSION_NUMBER = "version_number", } diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx index bb84ed99fcfd6..f4e6656a1b448 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx @@ -22,13 +22,13 @@ import { useCallback, useState } from "react"; import { useSearchParams } from "react-router-dom"; import { useTableURLState } from "src/components/DataTable/useTableUrlState"; +import { ResetButton } from "src/components/ui"; import { SearchParamsKeys, type SearchParamsKeysType } from "src/constants/searchParams"; import { useConfig } from "src/queries/useConfig"; import { useDagTagsInfinite } from "src/queries/useDagTagsInfinite"; import { FavoriteFilter } from "./FavoriteFilter"; import { PausedFilter } from "./PausedFilter"; -import { ResetButton } from "./ResetButton"; import { StateFilters } from "./StateFilters"; import { TagFilter } from "./TagFilter"; diff --git a/airflow-core/src/airflow/ui/src/pages/Events/Events.tsx b/airflow-core/src/airflow/ui/src/pages/Events/Events.tsx index d804ee30686e5..334f6c5e91773 100644 --- a/airflow-core/src/airflow/ui/src/pages/Events/Events.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Events/Events.tsx @@ -16,11 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -import { Box, ButtonGroup, Code, Flex, Heading, IconButton, useDisclosure } from "@chakra-ui/react"; +import { ButtonGroup, Code, Flex, Heading, IconButton, useDisclosure, VStack } from "@chakra-ui/react"; import type { ColumnDef } from "@tanstack/react-table"; +import dayjs from "dayjs"; import { useTranslation } from "react-i18next"; import { MdCompress, MdExpand } from "react-icons/md"; -import { useParams } from "react-router-dom"; +import { useParams, useSearchParams } from "react-router-dom"; import { useEventLogServiceGetEventLogs } from "openapi/queries"; import type { EventLogResponse } from "openapi/requests/types.gen"; @@ -29,6 +30,9 @@ import { useTableURLState } from "src/components/DataTable/useTableUrlState"; import { ErrorAlert } from "src/components/ErrorAlert"; import RenderedJsonField from "src/components/RenderedJsonField"; import Time from "src/components/Time"; +import { SearchParamsKeys, type SearchParamsKeysType } from "src/constants/searchParams"; + +import { EventsFilters } from "./EventsFilters"; type EventsColumn = { dagId?: string; @@ -141,31 +145,66 @@ const eventsColumn = ( }, ]; +const { + AFTER: AFTER_PARAM, + BEFORE: BEFORE_PARAM, + DAG_ID: DAG_ID_PARAM, + EVENT_TYPE: EVENT_TYPE_PARAM, + MAP_INDEX: MAP_INDEX_PARAM, + RUN_ID: RUN_ID_PARAM, + TASK_ID: TASK_ID_PARAM, + TRY_NUMBER: TRY_NUMBER_PARAM, + USER: USER_PARAM, +}: SearchParamsKeysType = SearchParamsKeys; + export const Events = () => { const { t: translate } = useTranslation("browse"); const { dagId, runId, taskId } = useParams(); + const [searchParams] = useSearchParams(); const { setTableURLState, tableURLState } = useTableURLState(); const { pagination, sorting } = tableURLState; const [sort] = sorting; const { onClose, onOpen, open } = useDisclosure(); + const afterFilter = searchParams.get(AFTER_PARAM); + const beforeFilter = searchParams.get(BEFORE_PARAM); + const dagIdFilter = searchParams.get(DAG_ID_PARAM); + const eventTypeFilter = searchParams.get(EVENT_TYPE_PARAM); + const mapIndexFilter = searchParams.get(MAP_INDEX_PARAM); + const runIdFilter = searchParams.get(RUN_ID_PARAM); + const taskIdFilter = searchParams.get(TASK_ID_PARAM); + const tryNumberFilter = searchParams.get(TRY_NUMBER_PARAM); + const userFilter = searchParams.get(USER_PARAM); + const orderBy = sort ? [`${sort.desc ? "-" : ""}${sort.id}`] : ["-when"]; + // Convert string filters to appropriate types for API + const mapIndexNumber = mapIndexFilter === null ? undefined : parseInt(mapIndexFilter, 10); + const tryNumberNumber = tryNumberFilter === null ? undefined : parseInt(tryNumberFilter, 10); + // Handle date conversion - ensure valid ISO strings + const afterDate = afterFilter !== null && dayjs(afterFilter).isValid() ? afterFilter : undefined; + const beforeDate = beforeFilter !== null && dayjs(beforeFilter).isValid() ? beforeFilter : undefined; const { data, error, isFetching, isLoading } = useEventLogServiceGetEventLogs( { - dagId, + after: afterDate, + before: beforeDate, + dagId: dagId ?? dagIdFilter ?? undefined, + event: eventTypeFilter ?? undefined, limit: pagination.pageSize, + mapIndex: mapIndexNumber, offset: pagination.pageIndex * pagination.pageSize, orderBy, - runId, - taskId, + owner: userFilter ?? undefined, + runId: runId ?? runIdFilter ?? undefined, + taskId: taskId ?? taskIdFilter ?? undefined, + tryNumber: tryNumberNumber, }, undefined, { enabled: !isNaN(pagination.pageSize) }, ); return ( - + {dagId === undefined && runId === undefined && taskId === undefined ? ( {translate("auditLog.title")} @@ -191,10 +230,13 @@ export const Events = () => { + + + { modelName={translate("auditLog.columns.event")} onStateChange={setTableURLState} skeletonCount={undefined} - total={data ? data.total_entries : 0} + total={data?.total_entries ?? 0} /> - + ); }; diff --git a/airflow-core/src/airflow/ui/src/pages/Events/EventsFilters.tsx b/airflow-core/src/airflow/ui/src/pages/Events/EventsFilters.tsx new file mode 100644 index 0000000000000..63707db09c2b8 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/pages/Events/EventsFilters.tsx @@ -0,0 +1,232 @@ +/*! + * 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, VStack, Text } from "@chakra-ui/react"; +import { useCallback } from "react"; +import { useTranslation } from "react-i18next"; +import { useSearchParams } from "react-router-dom"; + +import { useTableURLState } from "src/components/DataTable/useTableUrlState"; +import { DateTimeInput } from "src/components/DateTimeInput"; +import { SearchBar } from "src/components/SearchBar"; +import { ResetButton } from "src/components/ui"; +import { SearchParamsKeys } from "src/constants/searchParams"; + +const { + AFTER: AFTER_PARAM, + BEFORE: BEFORE_PARAM, + DAG_ID: DAG_ID_PARAM, + EVENT_TYPE: EVENT_TYPE_PARAM, + MAP_INDEX: MAP_INDEX_PARAM, + RUN_ID: RUN_ID_PARAM, + TASK_ID: TASK_ID_PARAM, + TRY_NUMBER: TRY_NUMBER_PARAM, + USER: USER_PARAM, +} = SearchParamsKeys; + +type EventsFiltersProps = { + readonly urlDagId?: string; + readonly urlRunId?: string; + readonly urlTaskId?: string; +}; + +export const EventsFilters = ({ urlDagId, urlRunId, urlTaskId }: EventsFiltersProps) => { + const { t: translate } = useTranslation(["browse", "common", "components"]); + const [searchParams, setSearchParams] = useSearchParams(); + const { setTableURLState, tableURLState } = useTableURLState(); + + const { pagination, sorting } = tableURLState; + + const resetPagination = useCallback(() => { + setTableURLState({ + pagination: { ...pagination, pageIndex: 0 }, + sorting, + }); + }, [pagination, setTableURLState, sorting]); + + const afterFilter = searchParams.get(AFTER_PARAM); + const beforeFilter = searchParams.get(BEFORE_PARAM); + const dagIdFilter = searchParams.get(DAG_ID_PARAM); + const eventTypeFilter = searchParams.get(EVENT_TYPE_PARAM); + const mapIndexFilter = searchParams.get(MAP_INDEX_PARAM); + const runIdFilter = searchParams.get(RUN_ID_PARAM); + const taskIdFilter = searchParams.get(TASK_ID_PARAM); + const tryNumberFilter = searchParams.get(TRY_NUMBER_PARAM); + const userFilter = searchParams.get(USER_PARAM); + + const updateSearchParams = useCallback( + (paramName: string, value: string) => { + if (value) { + searchParams.set(paramName, value); + } else { + searchParams.delete(paramName); + } + resetPagination(); + setSearchParams(searchParams); + }, + [resetPagination, searchParams, setSearchParams], + ); + + const handleClearFilters = useCallback(() => { + // Clear all URL params + resetPagination(); + setSearchParams(new URLSearchParams()); + }, [resetPagination, setSearchParams]); + + const handleSearchChange = useCallback( + (paramName: string) => (value: string) => { + updateSearchParams(paramName, value); + }, + [updateSearchParams], + ); + + const handleDateTimeChange = useCallback( + (paramName: string) => (event: React.ChangeEvent) => { + const { value } = event.target; + + updateSearchParams(paramName, value); + }, + [updateSearchParams], + ); + + const filterCount = searchParams.size; + + return ( + + + {/* Timestamp Range Filters */} + + + {translate("components:backfill.dateRangeFrom")} + + + + + + {translate("components:backfill.dateRangeTo")} + + + + + {/* Event Type Filter */} + + + + + {/* User Filter */} + + + + + {/* DAG ID Filter - Hide if URL already has dagId */} + {urlDagId === undefined && ( + + + + )} + + {/* Task ID Filter - Hide if URL already has taskId */} + {urlTaskId === undefined && ( + + + + )} + + {/* Run ID Filter - Hide if URL already has runId */} + {urlRunId === undefined && ( + + + + )} + + {/* Map Index Filter */} + + + + + {/* Try Number Filter */} + + + + + + + + + + ); +};