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"),
         },

Reply via email to