This is an automated email from the ASF dual-hosted git repository.
pierrejeambrun pushed a commit to branch v3-1-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-1-test by this push:
new 91cf84df37b Convert Tasks Table from card to table mode (#60830)
(#60874)
91cf84df37b is described below
commit 91cf84df37b1d51f11443275d50272494b9baa16
Author: Pierre Jeambrun <[email protected]>
AuthorDate: Wed Jan 21 16:26:01 2026 +0100
Convert Tasks Table from card to table mode (#60830) (#60874)
* Convert Tasks Table from card to table mode
* Fix e2e tests
(cherry picked from commit 8bbe0fc495fc1ed405963964fd549e0ec1cdcf58)
---
.../airflow/ui/src/pages/Dag/Tasks/TaskCard.tsx | 99 ----------------------
.../ui/src/pages/Dag/Tasks/TaskRecentRuns.tsx | 71 ----------------
.../src/airflow/ui/src/pages/Dag/Tasks/Tasks.tsx | 61 ++++++++++---
.../pages/HITLTaskInstances/HITLTaskInstances.tsx | 13 +--
4 files changed, 55 insertions(+), 189 deletions(-)
diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Tasks/TaskCard.tsx
b/airflow-core/src/airflow/ui/src/pages/Dag/Tasks/TaskCard.tsx
deleted file mode 100644
index 83f1635cbc3..00000000000
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Tasks/TaskCard.tsx
+++ /dev/null
@@ -1,99 +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.
- */
-import { Heading, VStack, Box, SimpleGrid, Text, Link } from
"@chakra-ui/react";
-import { useTranslation } from "react-i18next";
-import { Link as RouterLink } from "react-router-dom";
-
-import { useTaskInstanceServiceGetTaskInstances } from
"openapi/queries/queries.ts";
-import type { TaskResponse } from "openapi/requests/types.gen";
-import { StateBadge } from "src/components/StateBadge";
-import TaskInstanceTooltip from "src/components/TaskInstanceTooltip";
-import Time from "src/components/Time";
-import { isStatePending, useAutoRefresh } from "src/utils";
-import { getTaskInstanceLink } from "src/utils/links";
-
-import { TaskRecentRuns } from "./TaskRecentRuns.tsx";
-
-type Props = {
- readonly dagId: string;
- readonly task: TaskResponse;
-};
-
-export const TaskCard = ({ dagId, task }: Props) => {
- const { t: translate } = useTranslation();
- const refetchInterval = useAutoRefresh({ dagId });
-
- const { data } = useTaskInstanceServiceGetTaskInstances(
- {
- dagId,
- dagRunId: "~",
- limit: 14,
- orderBy: ["-run_after"],
- taskId: task.task_id ?? "",
- },
- undefined,
- {
- enabled: Boolean(dagId) && Boolean(task.task_id),
- refetchInterval: (query) =>
- query.state.data?.task_instances.some((ti) =>
isStatePending(ti.state)) ? refetchInterval : false,
- },
- );
-
- return (
- <Box borderColor="border.emphasized" borderRadius={8} borderWidth={1}
overflow="hidden" px={3} py={2}>
- <Link asChild color="fg.info" fontWeight="bold">
- <RouterLink to={`/dags/${dagId}/tasks/${task.task_id}`}>
- {task.task_display_name ?? task.task_id}
- {task.is_mapped ? "[]" : undefined}
- </RouterLink>
- </Link>
- <SimpleGrid columns={4} gap={4} height={20}>
- <VStack align="flex-start" gap={1}>
- <Heading color="fg.muted" fontSize="xs">
- {translate("task.operator")}
- </Heading>
- <Text fontSize="sm">{task.operator_name}</Text>
- </VStack>
- <VStack align="flex-start" gap={1}>
- <Heading color="fg.muted" fontSize="xs">
- {translate("task.triggerRule")}
- </Heading>
- <Text fontSize="sm">{task.trigger_rule}</Text>
- </VStack>
- <VStack align="flex-start" gap={1}>
- <Heading color="fg.muted" fontSize="xs">
- {translate("task.lastInstance")}
- </Heading>
- {data?.task_instances[0] ? (
- <TaskInstanceTooltip taskInstance={data.task_instances[0]}>
- <Link asChild color="fg.info" fontSize="sm">
- <RouterLink to={getTaskInstanceLink(data.task_instances[0])}>
- <Time datetime={data.task_instances[0].start_date} />
- <StateBadge state={data.task_instances[0].state} />
- </RouterLink>
- </Link>
- </TaskInstanceTooltip>
- ) : undefined}
- </VStack>
- {/* TODO: Handled mapped tasks to not plot each map index as a task
instance */}
- {!task.is_mapped && <TaskRecentRuns
taskInstances={data?.task_instances ?? []} />}
- </SimpleGrid>
- </Box>
- );
-};
diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Tasks/TaskRecentRuns.tsx
b/airflow-core/src/airflow/ui/src/pages/Dag/Tasks/TaskRecentRuns.tsx
deleted file mode 100644
index 8dd9e70ad8d..00000000000
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Tasks/TaskRecentRuns.tsx
+++ /dev/null
@@ -1,71 +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.
- */
-import { Box, Flex } from "@chakra-ui/react";
-import dayjs from "dayjs";
-import duration from "dayjs/plugin/duration";
-import { Link } from "react-router-dom";
-
-import type { TaskInstanceResponse } from "openapi/requests/types.gen";
-import TaskInstanceTooltip from "src/components/TaskInstanceTooltip";
-import { getTaskInstanceLink } from "src/utils/links";
-
-dayjs.extend(duration);
-
-const BAR_HEIGHT = 60;
-
-export const TaskRecentRuns = ({
- taskInstances,
-}: {
- readonly taskInstances: Array<TaskInstanceResponse>;
-}) => {
- if (!taskInstances.length) {
- return undefined;
- }
-
- const taskInstancesWithDuration = taskInstances.map((taskInstance) => ({
- ...taskInstance,
- duration:
- dayjs.duration(dayjs(taskInstance.end_date ??
dayjs()).diff(taskInstance.start_date)).asSeconds() || 0,
- }));
-
- const max = Math.max.apply(
- undefined,
- taskInstancesWithDuration.map((taskInstance) => taskInstance.duration),
- );
-
- return (
- <Flex alignItems="flex-end" flexDirection="row-reverse">
- {taskInstancesWithDuration.map((taskInstance) => (
- <TaskInstanceTooltip key={taskInstance.dag_run_id}
taskInstance={taskInstance}>
- <Link to={getTaskInstanceLink(taskInstance)}>
- <Box p={1}>
- <Box
- bg={`${taskInstance.state ?? "none"}.solid`}
- borderRadius="4px"
- height={`${(taskInstance.duration / max) * BAR_HEIGHT}px`}
- minHeight={1}
- width="4px"
- />
- </Box>
- </Link>
- </TaskInstanceTooltip>
- ))}
- </Flex>
- );
-};
diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Tasks/Tasks.tsx
b/airflow-core/src/airflow/ui/src/pages/Dag/Tasks/Tasks.tsx
index c74723ce775..154e0394a2c 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Tasks/Tasks.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Tasks/Tasks.tsx
@@ -16,25 +16,63 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { Skeleton, Box } from "@chakra-ui/react";
+import { Box, Link } from "@chakra-ui/react";
+import type { ColumnDef } from "@tanstack/react-table";
+import type { TFunction } from "i18next";
+import { useTranslation } from "react-i18next";
import { useParams, useSearchParams } from "react-router-dom";
+import { Link as RouterLink } from "react-router-dom";
import { useTaskServiceGetTasks } from "openapi/queries";
import type { TaskResponse } from "openapi/requests/types.gen";
import { DataTable } from "src/components/DataTable";
-import type { CardDef } from "src/components/DataTable/types";
import { ErrorAlert } from "src/components/ErrorAlert";
+import { TruncatedText } from "src/components/TruncatedText";
import { SearchParamsKeys } from "src/constants/searchParams.ts";
import { TaskFilters } from "src/pages/Dag/Tasks/TaskFilters/TaskFilters.tsx";
-import { TaskCard } from "./TaskCard";
+type TaskRow = { row: { original: TaskResponse } };
-const cardDef = (dagId: string): CardDef<TaskResponse> => ({
- card: ({ row }) => <TaskCard dagId={dagId} task={row} />,
- meta: {
- customSkeleton: <Skeleton height="120px" width="100%" />,
+const createColumns = ({
+ dagId,
+ translate,
+}: {
+ dagId: string;
+ translate: TFunction;
+}): Array<ColumnDef<TaskResponse>> => [
+ {
+ accessorKey: "task_display_name",
+ cell: ({ row: { original } }: TaskRow) => (
+ <Link asChild color="fg.info" fontWeight="bold">
+ <RouterLink to={`/dags/${dagId}/tasks/${original.task_id}`}>
+ <TruncatedText text={original.task_display_name ?? original.task_id
?? ""} />
+ </RouterLink>
+ </Link>
+ ),
+ enableSorting: false,
+ header: translate("common:taskId"),
},
-});
+ {
+ accessorKey: "trigger_rule",
+ enableSorting: false,
+ header: translate("common:task.triggerRule"),
+ },
+ {
+ accessorKey: "operator_name",
+ enableSorting: false,
+ header: translate("common:task.operator"),
+ },
+ {
+ accessorKey: "retries",
+ enableSorting: false,
+ header: translate("tasks:retries"),
+ },
+ {
+ accessorKey: "is_mapped",
+ enableSorting: false,
+ header: translate("tasks:mapped"),
+ },
+];
export const Tasks = () => {
const { dagId = "" } = useParams();
@@ -46,6 +84,10 @@ export const Tasks = () => {
const selectedMapped = searchParams.get(MAPPED) ?? undefined;
const namePattern = searchParams.get(NAME_PATTERN) ?? undefined;
+ const { t: translate } = useTranslation(["tasks", "common"]);
+
+ const columns = createColumns({ dagId, translate });
+
const {
data,
error: tasksError,
@@ -92,8 +134,7 @@ export const Tasks = () => {
<TaskFilters tasksData={data} />
<DataTable
- cardDef={cardDef(dagId)}
- columns={[]}
+ columns={columns}
data={filteredTasks}
displayMode="card"
isFetching={isFetching}
diff --git
a/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.tsx
b/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.tsx
index ad4d885cce6..c43325f0369 100644
---
a/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.tsx
+++
b/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.tsx
@@ -38,7 +38,7 @@ import { getTaskInstanceLink } from "src/utils/links";
import { HITLFilters } from "./HITLFilters";
-type TaskInstanceRow = { row: { original: HITLDetail } };
+type HITLRow = { row: { original: HITLDetail } };
const {
DAG_DISPLAY_NAME_PATTERN,
@@ -60,14 +60,14 @@ const taskInstanceColumns = ({
}): Array<ColumnDef<HITLDetail>> => [
{
accessorKey: "task_instance_state",
- cell: ({ row: { original } }: TaskInstanceRow) => (
+ cell: ({ row: { original } }: HITLRow) => (
<StateBadge
state={original.task_instance.state}>{getHITLState(translate,
original)}</StateBadge>
),
header: translate("requiredActionState"),
},
{
accessorKey: "subject",
- cell: ({ row: { original } }: TaskInstanceRow) => (
+ cell: ({ row: { original } }: HITLRow) => (
<Link asChild color="fg.info" fontWeight="bold">
<RouterLink
to={`${getTaskInstanceLink(original.task_instance)}/required_actions`}>
<TruncatedText text={original.subject} />
@@ -90,9 +90,7 @@ const taskInstanceColumns = ({
: [
{
accessorKey: "run_after",
- cell: ({ row: { original } }: TaskInstanceRow) => (
- <Time datetime={original.task_instance.run_after} />
- ),
+ cell: ({ row: { original } }: HITLRow) => <Time
datetime={original.task_instance.run_after} />,
header: translate("common:dagRun.runAfter"),
},
]),
@@ -101,9 +99,6 @@ const taskInstanceColumns = ({
: [
{
accessorKey: "task_display_name",
- cell: ({ row: { original } }: TaskInstanceRow) => (
- <TruncatedText text={original.task_instance.task_display_name} />
- ),
enableSorting: false,
header: translate("common:taskId"),
},