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 05b23057aed Add autorefresh to Task Instance and Dag Run pages (#46213)
05b23057aed is described below

commit 05b23057aedcc5a58669293a64ebd7dc630e1b58
Author: Brent Bovenzi <[email protected]>
AuthorDate: Wed Jan 29 11:45:34 2025 -0500

    Add autorefresh to Task Instance and Dag Run pages (#46213)
    
    * Add autorefresh to task instance page
    
    * Add refresh indicator
    
    * Add dag run page
    
    * Update airflow/ui/src/components/TaskTrySelect.tsx
    
    Co-authored-by: Pierre Jeambrun <[email protected]>
    
    ---------
    
    Co-authored-by: Pierre Jeambrun <[email protected]>
---
 airflow/ui/src/components/StateBadge.tsx           |  4 +--
 airflow/ui/src/components/TaskTrySelect.tsx        | 11 +++++++
 airflow/ui/src/components/ui/ProgressBar.tsx       |  4 ++-
 airflow/ui/src/pages/Run/Header.tsx                | 14 +++++---
 airflow/ui/src/pages/Run/Run.tsx                   | 23 ++++++++++---
 airflow/ui/src/pages/Run/TaskInstances.tsx         | 12 ++++++-
 airflow/ui/src/pages/TaskInstance/Details.tsx      | 27 ++++++++++-----
 airflow/ui/src/pages/TaskInstance/Header.tsx       | 14 +++++---
 airflow/ui/src/pages/TaskInstance/Logs/Logs.tsx    |  6 ++--
 airflow/ui/src/pages/TaskInstance/TaskInstance.tsx | 30 +++++++++++++----
 airflow/ui/src/queries/useClearTaskInstances.ts    |  9 +++--
 airflow/ui/src/queries/useLogs.tsx                 | 38 +++++++++++++++-------
 .../ui/ProgressBar.tsx => utils/refresh.ts}        | 19 ++++++-----
 13 files changed, 148 insertions(+), 63 deletions(-)

diff --git a/airflow/ui/src/components/StateBadge.tsx 
b/airflow/ui/src/components/StateBadge.tsx
index 691c9a4aac2..a48b56b8a99 100644
--- a/airflow/ui/src/components/StateBadge.tsx
+++ b/airflow/ui/src/components/StateBadge.tsx
@@ -30,7 +30,7 @@ export type Props = {
 export const StateBadge = React.forwardRef<HTMLDivElement, Props>(({ children, 
state, ...rest }, ref) => (
   <Badge
     borderRadius="full"
-    colorPalette={state ?? undefined}
+    colorPalette={state === null ? "none" : state}
     fontSize="sm"
     px={children === undefined ? 1 : 2}
     py={1}
@@ -38,7 +38,7 @@ export const StateBadge = React.forwardRef<HTMLDivElement, 
Props>(({ children, s
     variant="solid"
     {...rest}
   >
-    {state ? <StateIcon state={state} /> : undefined}
+    {state === undefined ? undefined : <StateIcon state={state} />}
     {children}
   </Badge>
 ));
diff --git a/airflow/ui/src/components/TaskTrySelect.tsx 
b/airflow/ui/src/components/TaskTrySelect.tsx
index 88e32f9f9e3..f4b4cf6c33c 100644
--- a/airflow/ui/src/components/TaskTrySelect.tsx
+++ b/airflow/ui/src/components/TaskTrySelect.tsx
@@ -21,6 +21,8 @@ import { Button, createListCollection, HStack, VStack, 
Heading } from "@chakra-u
 import { useTaskInstanceServiceGetMappedTaskInstanceTries } from 
"openapi/queries";
 import type { TaskInstanceHistoryResponse, TaskInstanceResponse } from 
"openapi/requests/types.gen";
 import { StateBadge } from "src/components/StateBadge";
+import { useConfig } from "src/queries/useConfig";
+import { isStatePending } from "src/utils/refresh";
 
 import TaskInstanceTooltip from "./TaskInstanceTooltip";
 import { Select } from "./ui";
@@ -36,10 +38,13 @@ export const TaskTrySelect = ({ onSelectTryNumber, 
selectedTryNumber, taskInstan
     dag_id: dagId,
     dag_run_id: dagRunId,
     map_index: mapIndex,
+    state,
     task_id: taskId,
     try_number: finalTryNumber,
   } = taskInstance;
 
+  const autoRefreshInterval = useConfig("auto_refresh_interval") as number;
+
   const { data: tiHistory } = useTaskInstanceServiceGetMappedTaskInstanceTries(
     {
       dagId,
@@ -50,6 +55,12 @@ export const TaskTrySelect = ({ onSelectTryNumber, 
selectedTryNumber, taskInstan
     undefined,
     {
       enabled: Boolean(finalTryNumber && finalTryNumber > 1), // Only try to 
look up task tries if try number > 1
+      refetchInterval: (query) =>
+        // We actually want to use || here
+        // eslint-disable-next-line 
@typescript-eslint/prefer-nullish-coalescing
+        query.state.data?.task_instances.some((ti) => 
isStatePending(ti.state)) || isStatePending(state)
+          ? autoRefreshInterval * 1000
+          : false,
     },
   );
 
diff --git a/airflow/ui/src/components/ui/ProgressBar.tsx 
b/airflow/ui/src/components/ui/ProgressBar.tsx
index 24788c1e3e6..4aad71c59d2 100644
--- a/airflow/ui/src/components/ui/ProgressBar.tsx
+++ b/airflow/ui/src/components/ui/ProgressBar.tsx
@@ -20,7 +20,9 @@ import { Progress as ChakraProgress } from "@chakra-ui/react";
 import { forwardRef } from "react";
 
 export const ProgressBar = forwardRef<HTMLDivElement, 
ChakraProgress.RootProps>((props, ref) => (
-  <ChakraProgress.Root {...props} ref={ref}>
+  // default to indeterminate
+  // eslint-disable-next-line unicorn/no-null
+  <ChakraProgress.Root value={null} {...props} ref={ref}>
     <ChakraProgress.Track>
       <ChakraProgress.Range />
     </ChakraProgress.Track>
diff --git a/airflow/ui/src/pages/Run/Header.tsx 
b/airflow/ui/src/pages/Run/Header.tsx
index 22bcf9386ec..493b2d43d3d 100644
--- a/airflow/ui/src/pages/Run/Header.tsx
+++ b/airflow/ui/src/pages/Run/Header.tsx
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Box, Flex, Heading, HStack, SimpleGrid, Text } from 
"@chakra-ui/react";
+import { Box, Flex, Heading, HStack, SimpleGrid, Spinner, Text } from 
"@chakra-ui/react";
 import { FiBarChart, FiMessageSquare } from "react-icons/fi";
 
 import type { DAGRunResponse } from "openapi/requests/types.gen";
@@ -29,7 +29,13 @@ import { StateBadge } from "src/components/StateBadge";
 import Time from "src/components/Time";
 import { getDuration } from "src/utils";
 
-export const Header = ({ dagRun }: { readonly dagRun: DAGRunResponse }) => (
+export const Header = ({
+  dagRun,
+  isRefreshing,
+}: {
+  readonly dagRun: DAGRunResponse;
+  readonly isRefreshing?: boolean;
+}) => (
   <Box borderColor="border" borderRadius={8} borderWidth={1} p={2}>
     <Flex alignItems="center" justifyContent="space-between" mb={2}>
       <HStack alignItems="center" gap={2}>
@@ -39,9 +45,7 @@ export const Header = ({ dagRun }: { readonly dagRun: 
DAGRunResponse }) => (
           {dagRun.dag_run_id}
         </Heading>
         <StateBadge state={dagRun.state}>{dagRun.state}</StateBadge>
-        <Flex>
-          <div />
-        </Flex>
+        {isRefreshing ? <Spinner /> : <div />}
       </HStack>
       <HStack>
         {dagRun.note === null || dagRun.note.length === 0 ? undefined : (
diff --git a/airflow/ui/src/pages/Run/Run.tsx b/airflow/ui/src/pages/Run/Run.tsx
index abc935a3513..8e2d8ef6f3b 100644
--- a/airflow/ui/src/pages/Run/Run.tsx
+++ b/airflow/ui/src/pages/Run/Run.tsx
@@ -22,6 +22,8 @@ import { useParams, Link as RouterLink } from 
"react-router-dom";
 import { useDagRunServiceGetDagRun, useDagServiceGetDagDetails } from 
"openapi/queries";
 import { Breadcrumb } from "src/components/ui";
 import { DetailsLayout } from "src/layouts/Details/DetailsLayout";
+import { useConfig } from "src/queries/useConfig";
+import { isStatePending } from "src/utils/refresh";
 
 import { Header } from "./Header";
 
@@ -35,14 +37,23 @@ const tabs = [
 export const Run = () => {
   const { dagId = "", runId = "" } = useParams();
 
+  const autoRefreshInterval = useConfig("auto_refresh_interval") as number;
+
   const {
     data: dagRun,
     error,
     isLoading,
-  } = useDagRunServiceGetDagRun({
-    dagId,
-    dagRunId: runId,
-  });
+  } = useDagRunServiceGetDagRun(
+    {
+      dagId,
+      dagRunId: runId,
+    },
+    undefined,
+    {
+      refetchInterval: (query) =>
+        isStatePending(query.state.data?.state) ? autoRefreshInterval * 1000 : 
false,
+    },
+  );
 
   const {
     data: dag,
@@ -63,7 +74,9 @@ export const Run = () => {
         </Breadcrumb.Link>
         <Breadcrumb.CurrentLink>{runId}</Breadcrumb.CurrentLink>
       </Breadcrumb.Root>
-      {dagRun === undefined ? undefined : <Header dagRun={dagRun} />}
+      {dagRun === undefined ? undefined : (
+        <Header dagRun={dagRun} 
isRefreshing={Boolean(isStatePending(dagRun.state) && autoRefreshInterval)} />
+      )}
     </DetailsLayout>
   );
 };
diff --git a/airflow/ui/src/pages/Run/TaskInstances.tsx 
b/airflow/ui/src/pages/Run/TaskInstances.tsx
index 4f9667091df..e7e57375b9e 100644
--- a/airflow/ui/src/pages/Run/TaskInstances.tsx
+++ b/airflow/ui/src/pages/Run/TaskInstances.tsx
@@ -40,8 +40,10 @@ import { StateBadge } from "src/components/StateBadge";
 import Time from "src/components/Time";
 import { Select } from "src/components/ui";
 import { SearchParamsKeys, type SearchParamsKeysType } from 
"src/constants/searchParams";
+import { useConfig } from "src/queries/useConfig";
 import { capitalize, getDuration } from "src/utils";
 import { getTaskInstanceLink } from "src/utils/links";
+import { isStatePending } from "src/utils/refresh";
 
 const columns: Array<ColumnDef<TaskInstanceResponse>> = [
   {
@@ -178,6 +180,8 @@ export const TaskInstances = () => {
     setSearchParams(searchParams);
   };
 
+  const autoRefreshInterval = useConfig("auto_refresh_interval") as number;
+
   const { data, error, isFetching, isLoading } = 
useTaskInstanceServiceGetTaskInstances(
     {
       dagId,
@@ -189,7 +193,13 @@ export const TaskInstances = () => {
       taskDisplayNamePattern: Boolean(taskDisplayNamePattern) ? 
taskDisplayNamePattern : undefined,
     },
     undefined,
-    { enabled: !isNaN(pagination.pageSize) },
+    {
+      enabled: !isNaN(pagination.pageSize),
+      refetchInterval: (query) =>
+        query.state.data?.task_instances.some((ti) => isStatePending(ti.state))
+          ? autoRefreshInterval * 1000
+          : false,
+    },
   );
 
   return (
diff --git a/airflow/ui/src/pages/TaskInstance/Details.tsx 
b/airflow/ui/src/pages/TaskInstance/Details.tsx
index 28d906c6080..dc7074f1647 100644
--- a/airflow/ui/src/pages/TaskInstance/Details.tsx
+++ b/airflow/ui/src/pages/TaskInstance/Details.tsx
@@ -27,7 +27,9 @@ import { StateBadge } from "src/components/StateBadge";
 import { TaskTrySelect } from "src/components/TaskTrySelect";
 import Time from "src/components/Time";
 import { ClipboardRoot, ClipboardIconButton } from "src/components/ui";
+import { useConfig } from "src/queries/useConfig";
 import { getDuration } from "src/utils";
+import { isStatePending } from "src/utils/refresh";
 
 export const Details = () => {
   const { dagId = "", runId = "", taskId = "" } = useParams();
@@ -37,6 +39,8 @@ export const Details = () => {
   const tryNumberParam = searchParams.get("try_number");
   const mapIndex = parseInt(mapIndexParam ?? "-1", 10);
 
+  const autoRefreshInterval = useConfig("auto_refresh_interval") as number;
+
   const { data: taskInstance } = useTaskInstanceServiceGetMappedTaskInstance({
     dagId,
     dagRunId: runId,
@@ -55,13 +59,20 @@ export const Details = () => {
 
   const tryNumber = tryNumberParam === null ? taskInstance?.try_number : 
parseInt(tryNumberParam, 10);
 
-  const { data: tryInstance } = 
useTaskInstanceServiceGetTaskInstanceTryDetails({
-    dagId,
-    dagRunId: runId,
-    mapIndex,
-    taskId,
-    taskTryNumber: tryNumber ?? 1,
-  });
+  const { data: tryInstance } = 
useTaskInstanceServiceGetTaskInstanceTryDetails(
+    {
+      dagId,
+      dagRunId: runId,
+      mapIndex,
+      taskId,
+      taskTryNumber: tryNumber ?? 1,
+    },
+    undefined,
+    {
+      refetchInterval: (query) =>
+        isStatePending(query.state.data?.state) ? autoRefreshInterval * 1000 : 
false,
+    },
+  );
 
   return (
     <Box p={2}>
@@ -77,7 +88,7 @@ export const Details = () => {
       <Table.Root striped>
         <Table.Body>
           <Table.Row>
-            <Table.Cell>StateBadge</Table.Cell>
+            <Table.Cell>State</Table.Cell>
             <Table.Cell>
               <Flex gap={1}>
                 <StateBadge state={tryInstance?.state} />
diff --git a/airflow/ui/src/pages/TaskInstance/Header.tsx 
b/airflow/ui/src/pages/TaskInstance/Header.tsx
index f8bdbe86042..a4a97fcbb1b 100644
--- a/airflow/ui/src/pages/TaskInstance/Header.tsx
+++ b/airflow/ui/src/pages/TaskInstance/Header.tsx
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Box, Flex, Heading, HStack, SimpleGrid } from "@chakra-ui/react";
+import { Box, Flex, Heading, HStack, SimpleGrid, Spinner } from 
"@chakra-ui/react";
 import { FiMessageSquare } from "react-icons/fi";
 import { MdOutlineTask } from "react-icons/md";
 
@@ -29,7 +29,13 @@ import { StateBadge } from "src/components/StateBadge";
 import Time from "src/components/Time";
 import { getDuration } from "src/utils";
 
-export const Header = ({ taskInstance }: { readonly taskInstance: 
TaskInstanceResponse }) => (
+export const Header = ({
+  isRefreshing,
+  taskInstance,
+}: {
+  readonly isRefreshing?: boolean;
+  readonly taskInstance: TaskInstanceResponse;
+}) => (
   <Box borderColor="border" borderRadius={8} borderWidth={1} p={2}>
     <Flex alignItems="center" justifyContent="space-between" mb={2}>
       <HStack alignItems="center" gap={2}>
@@ -39,9 +45,7 @@ export const Header = ({ taskInstance }: { readonly 
taskInstance: TaskInstanceRe
           {taskInstance.task_display_name} <Time 
datetime={taskInstance.start_date} />
         </Heading>
         <StateBadge 
state={taskInstance.state}>{taskInstance.state}</StateBadge>
-        <Flex>
-          <div />
-        </Flex>
+        {isRefreshing ? <Spinner /> : <div />}
       </HStack>
       <HStack>
         {taskInstance.note === null || taskInstance.note.length === 0 ? 
undefined : (
diff --git a/airflow/ui/src/pages/TaskInstance/Logs/Logs.tsx 
b/airflow/ui/src/pages/TaskInstance/Logs/Logs.tsx
index 9a58059f7b1..c062c7f4b2b 100644
--- a/airflow/ui/src/pages/TaskInstance/Logs/Logs.tsx
+++ b/airflow/ui/src/pages/TaskInstance/Logs/Logs.tsx
@@ -76,10 +76,8 @@ export const Logs = () => {
     isLoading: isLoadingLogs,
   } = useLogs({
     dagId,
-    mapIndex,
-    runId,
-    taskId,
-    tryNumber: tryNumber ?? 1,
+    taskInstance,
+    tryNumber: tryNumber === 0 ? 1 : tryNumber,
   });
 
   return (
diff --git a/airflow/ui/src/pages/TaskInstance/TaskInstance.tsx 
b/airflow/ui/src/pages/TaskInstance/TaskInstance.tsx
index 699aec0ebce..fc5b46883ed 100644
--- a/airflow/ui/src/pages/TaskInstance/TaskInstance.tsx
+++ b/airflow/ui/src/pages/TaskInstance/TaskInstance.tsx
@@ -22,6 +22,8 @@ import { useParams, Link as RouterLink, useSearchParams } 
from "react-router-dom
 import { useDagServiceGetDagDetails, 
useTaskInstanceServiceGetMappedTaskInstance } from "openapi/queries";
 import { Breadcrumb } from "src/components/ui";
 import { DetailsLayout } from "src/layouts/Details/DetailsLayout";
+import { useConfig } from "src/queries/useConfig";
+import { isStatePending } from "src/utils/refresh";
 
 import { Header } from "./Header";
 
@@ -40,16 +42,25 @@ export const TaskInstance = () => {
   const mapIndexParam = searchParams.get("map_index");
   const mapIndex = parseInt(mapIndexParam ?? "-1", 10);
 
+  const autoRefreshInterval = useConfig("auto_refresh_interval") as number;
+
   const {
     data: taskInstance,
     error,
     isLoading,
-  } = useTaskInstanceServiceGetMappedTaskInstance({
-    dagId,
-    dagRunId: runId,
-    mapIndex,
-    taskId,
-  });
+  } = useTaskInstanceServiceGetMappedTaskInstance(
+    {
+      dagId,
+      dagRunId: runId,
+      mapIndex,
+      taskId,
+    },
+    undefined,
+    {
+      refetchInterval: (query) =>
+        isStatePending(query.state.data?.state) ? autoRefreshInterval * 1000 : 
false,
+    },
+  );
 
   const {
     data: dag,
@@ -89,7 +100,12 @@ export const TaskInstance = () => {
           );
         })}
       </Breadcrumb.Root>
-      {taskInstance === undefined ? undefined : <Header 
taskInstance={taskInstance} />}
+      {taskInstance === undefined ? undefined : (
+        <Header
+          isRefreshing={Boolean(isStatePending(taskInstance.state) && 
autoRefreshInterval)}
+          taskInstance={taskInstance}
+        />
+      )}
     </DetailsLayout>
   );
 };
diff --git a/airflow/ui/src/queries/useClearTaskInstances.ts 
b/airflow/ui/src/queries/useClearTaskInstances.ts
index 5a319ceef45..4c0e7be662b 100644
--- a/airflow/ui/src/queries/useClearTaskInstances.ts
+++ b/airflow/ui/src/queries/useClearTaskInstances.ts
@@ -21,8 +21,7 @@ import { useQueryClient } from "@tanstack/react-query";
 import {
   UseDagRunServiceGetDagRunKeyFn,
   useDagRunServiceGetDagRunsKey,
-  UseTaskInstanceServiceGetTaskInstanceKeyFn,
-  useTaskInstanceServiceGetTaskInstancesKey,
+  UseTaskInstanceServiceGetMappedTaskInstanceKeyFn,
   useTaskInstanceServicePostClearTaskInstances,
 } from "openapi/queries";
 import type { ClearTaskInstancesBody, TaskInstanceCollectionResponse } from 
"openapi/requests/types.gen";
@@ -64,16 +63,16 @@ export const useClearTaskInstances = ({
               return undefined;
             }
 
-            const params = { dagId, dagRunId: runId, taskId: actualTaskId };
+            // TODO: update mapIndex when the endpoint supports clearing 
mapped tasks
+            const params = { dagId, dagRunId: runId, mapIndex: -1, taskId: 
actualTaskId };
 
-            return UseTaskInstanceServiceGetTaskInstanceKeyFn(params);
+            return UseTaskInstanceServiceGetMappedTaskInstanceKeyFn(params);
           })
           .filter((key) => key !== undefined),
       ),
     ];
 
     const queryKeys = [
-      [useTaskInstanceServiceGetTaskInstancesKey],
       ...taskInstanceKeys,
       UseDagRunServiceGetDagRunKeyFn({ dagId, dagRunId }),
       [useDagRunServiceGetDagRunsKey],
diff --git a/airflow/ui/src/queries/useLogs.tsx 
b/airflow/ui/src/queries/useLogs.tsx
index 3f151d54fee..bf7a164dfa5 100644
--- a/airflow/ui/src/queries/useLogs.tsx
+++ b/airflow/ui/src/queries/useLogs.tsx
@@ -16,13 +16,17 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+import dayjs from "dayjs";
+
 import { useTaskInstanceServiceGetLog } from "openapi/queries";
+import type { TaskInstanceResponse } from "openapi/requests/types.gen";
+import { isStatePending } from "src/utils/refresh";
+
+import { useConfig } from "./useConfig";
 
 type Props = {
   dagId: string;
-  mapIndex?: number;
-  runId: string;
-  taskId: string;
+  taskInstance?: TaskInstanceResponse;
   tryNumber?: number;
 };
 
@@ -57,14 +61,26 @@ const parseLogs = ({ data }: ParseLogsProps) => {
   };
 };
 
-export const useLogs = ({ dagId, mapIndex = -1, runId, taskId, tryNumber = 1 
}: Props) => {
-  const { data, ...rest } = useTaskInstanceServiceGetLog({
-    dagId,
-    dagRunId: runId,
-    mapIndex,
-    taskId,
-    tryNumber,
-  });
+export const useLogs = ({ dagId, taskInstance, tryNumber = 1 }: Props) => {
+  const autoRefreshInterval = useConfig("auto_refresh_interval") as number;
+  const { data, ...rest } = useTaskInstanceServiceGetLog(
+    {
+      dagId,
+      dagRunId: taskInstance?.dag_run_id ?? "",
+      mapIndex: taskInstance?.map_index ?? -1,
+      taskId: taskInstance?.task_id ?? "",
+      tryNumber,
+    },
+    undefined,
+    {
+      enabled: Boolean(taskInstance),
+      refetchInterval: (query) =>
+        isStatePending(taskInstance?.state) ||
+        dayjs(query.state.dataUpdatedAt).isBefore(taskInstance?.end_date)
+          ? autoRefreshInterval * 1000
+          : autoRefreshInterval * 10 * 1000,
+    },
+  );
 
   const parsedData = parseLogs({
     data: data?.content,
diff --git a/airflow/ui/src/components/ui/ProgressBar.tsx 
b/airflow/ui/src/utils/refresh.ts
similarity index 69%
copy from airflow/ui/src/components/ui/ProgressBar.tsx
copy to airflow/ui/src/utils/refresh.ts
index 24788c1e3e6..d9ecb618c0e 100644
--- a/airflow/ui/src/components/ui/ProgressBar.tsx
+++ b/airflow/ui/src/utils/refresh.ts
@@ -16,13 +16,14 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Progress as ChakraProgress } from "@chakra-ui/react";
-import { forwardRef } from "react";
+import type { TaskInstanceState } from "openapi/requests/types.gen";
 
-export const ProgressBar = forwardRef<HTMLDivElement, 
ChakraProgress.RootProps>((props, ref) => (
-  <ChakraProgress.Root {...props} ref={ref}>
-    <ChakraProgress.Track>
-      <ChakraProgress.Range />
-    </ChakraProgress.Track>
-  </ChakraProgress.Root>
-));
+export const isStatePending = (state?: TaskInstanceState | null) =>
+  state === "deferred" ||
+  state === "scheduled" ||
+  state === "running" ||
+  state === "up_for_reschedule" ||
+  state === "up_for_retry" ||
+  state === "queued" ||
+  state === "restarting" ||
+  !Boolean(state);

Reply via email to