From 5b867f6bcd586be25d8c9e6023a8bbbaba2c7def Mon Sep 17 00:00:00 2001 From: Brent Bovenzi Date: Tue, 11 Mar 2025 18:40:53 -0400 Subject: [PATCH 1/2] Add pagination to Asset events list, add asset events to dag run and task instance details tab --- .../ui/src/components/Assets/AssetEvent.tsx | 2 +- .../ui/src/components/Assets/AssetEvents.tsx | 68 +++++++++++-------- .../ui/src/components/DataTable/DataTable.tsx | 5 +- airflow/ui/src/pages/Asset/Asset.tsx | 46 ++++++++++++- .../HistoricalMetrics/HistoricalMetrics.tsx | 13 +++- airflow/ui/src/pages/Run/Details.tsx | 25 +++++-- airflow/ui/src/pages/TaskInstance/Details.tsx | 16 +++++ .../TaskInstance/DownstreamAssetEvents.tsx | 18 +++++ 8 files changed, 152 insertions(+), 41 deletions(-) create mode 100644 airflow/ui/src/pages/TaskInstance/DownstreamAssetEvents.tsx diff --git a/airflow/ui/src/components/Assets/AssetEvent.tsx b/airflow/ui/src/components/Assets/AssetEvent.tsx index 6b0b71cffbd46..fc134169802ae 100644 --- a/airflow/ui/src/components/Assets/AssetEvent.tsx +++ b/airflow/ui/src/components/Assets/AssetEvent.tsx @@ -42,7 +42,7 @@ export const AssetEvent = ({ } return ( - + diff --git a/airflow/ui/src/components/Assets/AssetEvents.tsx b/airflow/ui/src/components/Assets/AssetEvents.tsx index 4097d442274f3..98e59a689e145 100644 --- a/airflow/ui/src/components/Assets/AssetEvents.tsx +++ b/airflow/ui/src/components/Assets/AssetEvents.tsx @@ -16,33 +16,46 @@ * specific language governing permissions and limitations * under the License. */ -import { Box, Heading, Flex, HStack, VStack, StackSeparator, Skeleton } from "@chakra-ui/react"; +import { Box, Heading, Flex, HStack, Skeleton } from "@chakra-ui/react"; import { createListCollection } from "@chakra-ui/react/collection"; import { FiDatabase } from "react-icons/fi"; -import { useAssetServiceGetAssetEvents } from "openapi/queries"; +import type { AssetEventCollectionResponse, AssetEventResponse } from "openapi/requests/types.gen"; import { StateBadge } from "src/components/StateBadge"; import { Select } from "src/components/ui"; +import { pluralize } from "src/utils"; +import { DataTable } from "../DataTable"; +import type { CardDef, TableState } from "../DataTable/types"; import { AssetEvent } from "./AssetEvent"; +const cardDef = (assetId?: number): CardDef => ({ + card: ({ row }) => , + meta: { + customSkeleton: , + }, +}); + type AssetEventProps = { readonly assetId?: number; + readonly data?: AssetEventCollectionResponse; readonly endDate?: string; - readonly orderBy?: string; - readonly setOrderBy?: React.Dispatch>; - readonly startDate?: string; + readonly isLoading?: boolean; + readonly setOrderBy?: (order: string) => void; + readonly setTableUrlState?: (state: TableState) => void; + readonly tableUrlState?: TableState; + readonly title?: string; }; -export const AssetEvents = ({ assetId, endDate, orderBy, setOrderBy, startDate }: AssetEventProps) => { - const { data, isLoading } = useAssetServiceGetAssetEvents({ - assetId, - limit: 6, - orderBy, - timestampGte: startDate, - timestampLte: endDate, - }); - +export const AssetEvents = ({ + assetId, + data, + isLoading, + setOrderBy, + setTableUrlState, + tableUrlState, + title, +}: AssetEventProps) => { const assetSortOptions = createListCollection({ items: [ { label: "Newest first", value: "-timestamp" }, @@ -51,7 +64,7 @@ export const AssetEvents = ({ assetId, endDate, orderBy, setOrderBy, startDate } }); return ( - + @@ -59,7 +72,7 @@ export const AssetEvents = ({ assetId, endDate, orderBy, setOrderBy, startDate } {data?.total_entries ?? " "} - Asset Events + {pluralize(title ?? "Asset Event", data?.total_entries ?? 0, undefined, true)} {setOrderBy === undefined ? undefined : ( @@ -85,17 +98,18 @@ export const AssetEvents = ({ assetId, endDate, orderBy, setOrderBy, startDate } )} - {isLoading ? ( - }> - {Array.from({ length: 5 }, (_, index) => index).map((index) => ( - - ))} - - ) : ( - }> - {data?.asset_events.map((event) => )} - - )} + ); }; diff --git a/airflow/ui/src/components/DataTable/DataTable.tsx b/airflow/ui/src/components/DataTable/DataTable.tsx index 86182edb0b8bb..d4ed640a46f7d 100644 --- a/airflow/ui/src/components/DataTable/DataTable.tsx +++ b/airflow/ui/src/components/DataTable/DataTable.tsx @@ -117,8 +117,9 @@ export const DataTable = ({ const display = displayMode === "card" && Boolean(cardDef) ? "card" : "table"; const hasRows = rows.length > 0; const hasPagination = - table.getState().pagination.pageIndex !== 0 || - (table.getState().pagination.pageIndex === 0 && rows.length !== total); + initialState?.pagination !== undefined && + (table.getState().pagination.pageIndex !== 0 || + (table.getState().pagination.pageIndex === 0 && rows.length !== total)); return ( <> diff --git a/airflow/ui/src/pages/Asset/Asset.tsx b/airflow/ui/src/pages/Asset/Asset.tsx index 80dbfa4a9d976..0a961d19d2c58 100644 --- a/airflow/ui/src/pages/Asset/Asset.tsx +++ b/airflow/ui/src/pages/Asset/Asset.tsx @@ -18,12 +18,14 @@ */ import { Box, HStack } from "@chakra-ui/react"; import { ReactFlowProvider } from "@xyflow/react"; +import { useCallback } from "react"; import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; import { useParams } from "react-router-dom"; -import { useAssetServiceGetAsset } from "openapi/queries"; +import { useAssetServiceGetAsset, useAssetServiceGetAssetEvents } from "openapi/queries"; import { AssetEvents } from "src/components/Assets/AssetEvents"; import { BreadcrumbStats } from "src/components/BreadcrumbStats"; +import { useTableURLState } from "src/components/DataTable/useTableUrlState"; import { ProgressBar, Toaster } from "src/components/ui"; import { AssetGraph } from "./AssetGraph"; @@ -33,6 +35,11 @@ import { Header } from "./Header"; export const Asset = () => { const { assetId } = useParams(); + const { setTableURLState, tableURLState } = useTableURLState(); + const { pagination, sorting } = tableURLState; + const [sort] = sorting; + const orderBy = sort ? `${sort.desc ? "-" : ""}${sort.id}` : "-timestamp"; + const { data: asset, isLoading } = useAssetServiceGetAsset( { assetId: assetId === undefined ? 0 : parseInt(assetId, 10) }, undefined, @@ -49,6 +56,32 @@ export const Asset = () => { }, ]; + const { data, isLoading: isLoadingEvents } = useAssetServiceGetAssetEvents( + { + assetId: asset?.id, + limit: pagination.pageSize, + offset: pagination.pageIndex * pagination.pageSize, + orderBy, + }, + undefined, + { enabled: Boolean(asset?.id) }, + ); + + const setOrderBy = useCallback( + (value: string) => { + setTableURLState({ + pagination, + sorting: [ + { + desc: value.startsWith("-"), + id: value.replace("-", ""), + }, + ], + }); + }, + [pagination, setTableURLState], + ); + return ( @@ -69,8 +102,15 @@ export const Asset = () => {
- - + + diff --git a/airflow/ui/src/pages/Dashboard/HistoricalMetrics/HistoricalMetrics.tsx b/airflow/ui/src/pages/Dashboard/HistoricalMetrics/HistoricalMetrics.tsx index 94f9e6d6e7c68..c98f85bd34723 100644 --- a/airflow/ui/src/pages/Dashboard/HistoricalMetrics/HistoricalMetrics.tsx +++ b/airflow/ui/src/pages/Dashboard/HistoricalMetrics/HistoricalMetrics.tsx @@ -21,7 +21,7 @@ import dayjs from "dayjs"; import { useState } from "react"; import { PiBooks } from "react-icons/pi"; -import { useDashboardServiceHistoricalMetrics } from "openapi/queries"; +import { useAssetServiceGetAssetEvents, useDashboardServiceHistoricalMetrics } from "openapi/queries"; import { AssetEvents } from "src/components/Assets/AssetEvents"; import { ErrorAlert } from "src/components/ErrorAlert"; import TimeRangeSelector from "src/components/TimeRangeSelector"; @@ -51,6 +51,13 @@ export const HistoricalMetrics = () => { ? Object.values(data.task_instance_states).reduce((partialSum, value) => partialSum + value, 0) : 0; + const { data: assetEventsData, isLoading: isLoadingAssetEvents } = useAssetServiceGetAssetEvents({ + limit: 6, + orderBy: assetSortBy, + timestampGte: startDate, + timestampLte: endDate, + }); + return ( @@ -90,10 +97,10 @@ export const HistoricalMetrics = () => { diff --git a/airflow/ui/src/pages/Run/Details.tsx b/airflow/ui/src/pages/Run/Details.tsx index 58ad59376be01..a7e1021a11b63 100644 --- a/airflow/ui/src/pages/Run/Details.tsx +++ b/airflow/ui/src/pages/Run/Details.tsx @@ -19,25 +19,40 @@ import { Box, Flex, HStack, Table, Text } from "@chakra-ui/react"; import { useParams } from "react-router-dom"; -import { useDagRunServiceGetDagRun } from "openapi/queries"; +import { useDagRunServiceGetDagRun, useDagRunServiceGetUpstreamAssetEvents } from "openapi/queries"; +import { AssetEvents } from "src/components/Assets/AssetEvents"; import RenderedJsonField from "src/components/RenderedJsonField"; import { RunTypeIcon } from "src/components/RunTypeIcon"; import { StateBadge } from "src/components/StateBadge"; import Time from "src/components/Time"; import { ClipboardRoot, ClipboardIconButton } from "src/components/ui"; -import { getDuration } from "src/utils"; +import { getDuration, isStatePending, useAutoRefresh } from "src/utils"; export const Details = () => { const { dagId = "", runId = "" } = useParams(); - const { data: dagRun } = useDagRunServiceGetDagRun({ - dagId, - dagRunId: runId, + const refetchInterval = useAutoRefresh({ dagId }); + + const { data: dagRun } = useDagRunServiceGetDagRun( + { + dagId, + dagRunId: runId, + }, + undefined, + { refetchInterval: (query) => (isStatePending(query.state.data?.state) ? refetchInterval : false) }, + ); + + const { data, isLoading } = useDagRunServiceGetUpstreamAssetEvents({ dagId, dagRunId: runId }, undefined, { + enabled: dagRun?.run_type === "asset_triggered", + refetchInterval: () => (isStatePending(dagRun?.state) ? refetchInterval : false), }); // TODO : Render DagRun configuration object return ( + {data === undefined || dagRun?.run_type !== "asset_triggered" ? undefined : ( + + )} {dagRun === undefined ? (
) : ( diff --git a/airflow/ui/src/pages/TaskInstance/Details.tsx b/airflow/ui/src/pages/TaskInstance/Details.tsx index a97675b2f4d86..8661413dc7c9b 100644 --- a/airflow/ui/src/pages/TaskInstance/Details.tsx +++ b/airflow/ui/src/pages/TaskInstance/Details.tsx @@ -20,9 +20,11 @@ import { Box, Flex, HStack, Table, Heading } from "@chakra-ui/react"; import { useParams, useSearchParams } from "react-router-dom"; import { + useAssetServiceGetAssetEvents, useTaskInstanceServiceGetMappedTaskInstance, useTaskInstanceServiceGetTaskInstanceTryDetails, } from "openapi/queries"; +import { AssetEvents } from "src/components/Assets/AssetEvents"; import { StateBadge } from "src/components/StateBadge"; import { TaskTrySelect } from "src/components/TaskTrySelect"; import Time from "src/components/Time"; @@ -72,8 +74,22 @@ export const Details = () => { }, ); + const { data: assetEventsData, isLoading: isLoadingAssetEvents } = useAssetServiceGetAssetEvents( + { + sourceDagId: dagId, + sourceMapIndex: parseInt(mapIndex, 10), + sourceRunId: runId, + sourceTaskId: taskId, + }, + undefined, + { + refetchInterval: () => (isStatePending(taskInstance?.state) ? refetchInterval : false), + }, + ); + return ( + {taskInstance === undefined || tryNumber === undefined || taskInstance.try_number <= 1 ? (
) : ( diff --git a/airflow/ui/src/pages/TaskInstance/DownstreamAssetEvents.tsx b/airflow/ui/src/pages/TaskInstance/DownstreamAssetEvents.tsx new file mode 100644 index 0000000000000..3d56d22bb3136 --- /dev/null +++ b/airflow/ui/src/pages/TaskInstance/DownstreamAssetEvents.tsx @@ -0,0 +1,18 @@ +/*! + * 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. + */ From e038d8a5d66072b649c6c963e2c65725505c6e27 Mon Sep 17 00:00:00 2001 From: Brent Bovenzi Date: Tue, 11 Mar 2025 18:45:07 -0400 Subject: [PATCH 2/2] Remove unused file --- .../TaskInstance/DownstreamAssetEvents.tsx | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 airflow/ui/src/pages/TaskInstance/DownstreamAssetEvents.tsx diff --git a/airflow/ui/src/pages/TaskInstance/DownstreamAssetEvents.tsx b/airflow/ui/src/pages/TaskInstance/DownstreamAssetEvents.tsx deleted file mode 100644 index 3d56d22bb3136..0000000000000 --- a/airflow/ui/src/pages/TaskInstance/DownstreamAssetEvents.tsx +++ /dev/null @@ -1,18 +0,0 @@ -/*! - * 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. - */