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);