This is an automated email from the ASF dual-hosted git repository.
bbovenzi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 23c3e7a0272 Add pagination to Asset events list, add asset events to
dag run and … (#47647)
23c3e7a0272 is described below
commit 23c3e7a02728ab325e8c3b23d665f574c452819b
Author: Brent Bovenzi <[email protected]>
AuthorDate: Tue Mar 11 22:08:01 2025 -0400
Add pagination to Asset events list, add asset events to dag run and …
(#47647)
* Add pagination to Asset events list, add asset events to dag run and task
instance details tab
* Remove unused file
---
airflow/ui/src/components/Assets/AssetEvent.tsx | 2 +-
airflow/ui/src/components/Assets/AssetEvents.tsx | 68 +++++++++++++---------
airflow/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 +++++
7 files changed, 134 insertions(+), 41 deletions(-)
diff --git a/airflow/ui/src/components/Assets/AssetEvent.tsx
b/airflow/ui/src/components/Assets/AssetEvent.tsx
index 6b0b71cffbd..fc134169802 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 (
- <Box fontSize={13} mt={1} w="full">
+ <Box borderBottomWidth={1} fontSize={13} mt={1} p={2}>
<Text fontWeight="bold">
<Time datetime={event.timestamp} />
</Text>
diff --git a/airflow/ui/src/components/Assets/AssetEvents.tsx
b/airflow/ui/src/components/Assets/AssetEvents.tsx
index 4097d442274..98e59a689e1 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<AssetEventResponse> => ({
+ card: ({ row }) => <AssetEvent assetId={assetId} event={row} />,
+ meta: {
+ customSkeleton: <Skeleton height="120px" width="100%" />,
+ },
+});
+
type AssetEventProps = {
readonly assetId?: number;
+ readonly data?: AssetEventCollectionResponse;
readonly endDate?: string;
- readonly orderBy?: string;
- readonly setOrderBy?: React.Dispatch<React.SetStateAction<string>>;
- 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 (
- <Box borderRadius={5} borderWidth={1} ml={2} pb={2}>
+ <Box borderBottomWidth={0} borderRadius={5} borderWidth={1} ml={2}>
<Flex justify="space-between" mr={1} mt={0} pl={3} pt={1}>
<HStack>
<StateBadge colorPalette="blue" fontSize="md" variant="solid">
@@ -59,7 +72,7 @@ export const AssetEvents = ({ assetId, endDate, orderBy,
setOrderBy, startDate }
{data?.total_entries ?? " "}
</StateBadge>
<Heading marginEnd="auto" size="md">
- Asset Events
+ {pluralize(title ?? "Asset Event", data?.total_entries ?? 0,
undefined, true)}
</Heading>
</HStack>
{setOrderBy === undefined ? undefined : (
@@ -85,17 +98,18 @@ export const AssetEvents = ({ assetId, endDate, orderBy,
setOrderBy, startDate }
</Select.Root>
)}
</Flex>
- {isLoading ? (
- <VStack px={3} separator={<StackSeparator />}>
- {Array.from({ length: 5 }, (_, index) => index).map((index) => (
- <Skeleton height={100} key={index} width="full" />
- ))}
- </VStack>
- ) : (
- <VStack px={3} separator={<StackSeparator />}>
- {data?.asset_events.map((event) => <AssetEvent assetId={assetId}
event={event} key={event.id} />)}
- </VStack>
- )}
+ <DataTable
+ cardDef={cardDef(assetId)}
+ columns={[]}
+ data={data?.asset_events ?? []}
+ displayMode="card"
+ initialState={tableUrlState}
+ isLoading={isLoading}
+ modelName="Asset Event"
+ onStateChange={setTableUrlState}
+ skeletonCount={5}
+ total={data?.total_entries}
+ />
</Box>
);
};
diff --git a/airflow/ui/src/components/DataTable/DataTable.tsx
b/airflow/ui/src/components/DataTable/DataTable.tsx
index 86182edb0b8..d4ed640a46f 100644
--- a/airflow/ui/src/components/DataTable/DataTable.tsx
+++ b/airflow/ui/src/components/DataTable/DataTable.tsx
@@ -117,8 +117,9 @@ export const DataTable = <TData,>({
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 80dbfa4a9d9..0a961d19d2c 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 (
<ReactFlowProvider>
<Toaster />
@@ -69,8 +102,15 @@ export const Asset = () => {
</PanelResizeHandle>
<Panel defaultSize={30} minSize={20}>
<Header asset={asset} />
- <Box h="100%" overflow="auto" px={2}>
- <AssetEvents assetId={asset?.id} />
+ <Box h="100%" overflow="auto" pt={2}>
+ <AssetEvents
+ assetId={asset?.id}
+ data={data}
+ isLoading={isLoadingEvents}
+ setOrderBy={setOrderBy}
+ setTableUrlState={setTableURLState}
+ tableUrlState={tableURLState}
+ />
</Box>
</Panel>
</PanelGroup>
diff --git
a/airflow/ui/src/pages/Dashboard/HistoricalMetrics/HistoricalMetrics.tsx
b/airflow/ui/src/pages/Dashboard/HistoricalMetrics/HistoricalMetrics.tsx
index 94f9e6d6e7c..c98f85bd347 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 (
<Box width="100%">
<Flex color="fg.muted" my={2}>
@@ -90,10 +97,10 @@ export const HistoricalMetrics = () => {
</GridItem>
<GridItem colSpan={{ base: 3 }}>
<AssetEvents
+ data={assetEventsData}
endDate={endDate}
- orderBy={assetSortBy}
+ isLoading={isLoadingAssetEvents}
setOrderBy={setAssetSortBy}
- startDate={startDate}
/>
</GridItem>
</SimpleGrid>
diff --git a/airflow/ui/src/pages/Run/Details.tsx
b/airflow/ui/src/pages/Run/Details.tsx
index 58ad59376be..a7e1021a11b 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 (
<Box p={2}>
+ {data === undefined || dagRun?.run_type !== "asset_triggered" ?
undefined : (
+ <AssetEvents data={data} isLoading={isLoading} title="Source Asset
Event" />
+ )}
{dagRun === undefined ? (
<div />
) : (
diff --git a/airflow/ui/src/pages/TaskInstance/Details.tsx
b/airflow/ui/src/pages/TaskInstance/Details.tsx
index a97675b2f4d..8661413dc7c 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 (
<Box p={2}>
+ <AssetEvents data={assetEventsData} isLoading={isLoadingAssetEvents}
title="Created Asset Event" />
{taskInstance === undefined || tryNumber === undefined ||
taskInstance.try_number <= 1 ? (
<div />
) : (