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 />
       ) : (

Reply via email to