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 83f1635cbc355..0000000000000
--- 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 (
-
-
-
- {task.task_display_name ?? task.task_id}
- {task.is_mapped ? "[]" : undefined}
-
-
-
-
-
- {translate("task.operator")}
-
- {task.operator_name}
-
-
-
- {translate("task.triggerRule")}
-
- {task.trigger_rule}
-
-
-
- {translate("task.lastInstance")}
-
- {data?.task_instances[0] ? (
-
-
-
-
-
-
-
-
- ) : undefined}
-
- {/* TODO: Handled mapped tasks to not plot each map index as a task instance */}
- {!task.is_mapped && }
-
-
- );
-};
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 8dd9e70ad8df3..0000000000000
--- 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;
-}) => {
- 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 (
-
- {taskInstancesWithDuration.map((taskInstance) => (
-
-
-
-
-
-
-
- ))}
-
- );
-};
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 c74723ce775c6..154e0394a2cb6 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 => ({
- card: ({ row }) => ,
- meta: {
- customSkeleton: ,
+const createColumns = ({
+ dagId,
+ translate,
+}: {
+ dagId: string;
+ translate: TFunction;
+}): Array> => [
+ {
+ accessorKey: "task_display_name",
+ cell: ({ row: { original } }: TaskRow) => (
+
+
+
+
+
+ ),
+ 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 = () => {
> => [
{
accessorKey: "task_instance_state",
- cell: ({ row: { original } }: TaskInstanceRow) => (
+ cell: ({ row: { original } }: HITLRow) => (
{getHITLState(translate, original)}
),
header: translate("requiredActionState"),
},
{
accessorKey: "subject",
- cell: ({ row: { original } }: TaskInstanceRow) => (
+ cell: ({ row: { original } }: HITLRow) => (
@@ -86,7 +86,7 @@ const taskInstanceColumns = ({
: [
{
accessorKey: "task_instance.dag_id",
- cell: ({ row: { original } }: TaskInstanceRow) => (
+ cell: ({ row: { original } }: HITLRow) => (
@@ -102,7 +102,7 @@ const taskInstanceColumns = ({
: [
{
accessorKey: "run_id",
- cell: ({ row: { original } }: TaskInstanceRow) => (
+ cell: ({ row: { original } }: HITLRow) => (
(
-
- ),
+ cell: ({ row: { original } }: HITLRow) => ,
header: translate("common:dagRun.runAfter"),
},
]),
@@ -130,7 +128,7 @@ const taskInstanceColumns = ({
: [
{
accessorKey: "task_display_name",
- cell: ({ row: { original } }: TaskInstanceRow) => (
+ cell: ({ row: { original } }: HITLRow) => (
diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts b/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts
index 51d0f23c81904..a5506fcf05cb1 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts
@@ -40,10 +40,8 @@ export class DagsPage extends BasePage {
public readonly triggerButton: Locator;
public readonly triggerRuleFilter: Locator;
- public get taskCards(): Locator {
- // CardList component renders a SimpleGrid with data-testid="card-list"
- // Individual cards are direct children (Box elements)
- return this.page.locator('[data-testid="card-list"] > div');
+ public get taskRows(): Locator {
+ return this.page.locator('[data-testid="table-list"] > tbody > tr');
}
public constructor(page: Page) {
@@ -166,7 +164,7 @@ export class DagsPage extends BasePage {
public async navigateToDagTasks(dagId: string): Promise {
await this.page.goto(`/dags/${dagId}/tasks`);
await this.page
- .locator("h2")
+ .locator("th")
.filter({ hasText: /^Operator$/ })
.first()
.waitFor({ state: "visible", timeout: 30_000 });
diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/dag-tasks.spec.ts b/airflow-core/src/airflow/ui/tests/e2e/specs/dag-tasks.spec.ts
index 4177d626a9677..46927a6a129a6 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/specs/dag-tasks.spec.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/dag-tasks.spec.ts
@@ -42,14 +42,13 @@ test.describe("Dag Tasks Tab", () => {
await dagPage.navigateToDagTasks(testDagId);
await expect(page).toHaveURL(/\/tasks$/);
- await expect(dagPage.taskCards.first()).toBeVisible();
+ await expect(dagPage.taskRows.first()).toBeVisible();
- const firstCard = dagPage.taskCards.first();
+ const firstRow = dagPage.taskRows.first();
- await expect(firstCard.locator("a").first()).toBeVisible();
- await expect(firstCard).toContainText("Operator");
- await expect(firstCard).toContainText("Trigger Rule");
- await expect(firstCard).toContainText("Last Instance");
+ await expect(firstRow.locator("a").first()).toBeVisible();
+ await expect(firstRow).toContainText("BashOperator");
+ await expect(firstRow).toContainText("all_success");
});
test("verify search tasks by name", async ({ page }) => {
@@ -57,7 +56,7 @@ test.describe("Dag Tasks Tab", () => {
await dagPage.navigateToDagTasks(testDagId);
- const firstTaskLink = dagPage.taskCards.first().locator("a").first();
+ const firstTaskLink = dagPage.taskRows.first().locator("a").first();
const taskName = await firstTaskLink.textContent();
if (taskName === null) {
@@ -66,8 +65,8 @@ test.describe("Dag Tasks Tab", () => {
await dagPage.searchBox.fill(taskName);
- await expect.poll(() => dagPage.taskCards.count(), { timeout: 20_000 }).toBe(1);
- await expect(dagPage.taskCards).toContainText(taskName);
+ await expect.poll(() => dagPage.taskRows.count(), { timeout: 20_000 }).toBe(1);
+ await expect(dagPage.taskRows).toContainText(taskName);
});
test("verify filter tasks by operator dropdown", async ({ page }) => {
@@ -85,11 +84,11 @@ test.describe("Dag Tasks Tab", () => {
await expect
.poll(
async () => {
- const count = await dagPage.taskCards.count();
+ const count = await dagPage.taskRows.count();
if (count === 0) return false;
for (let i = 0; i < count; i++) {
- const text = await dagPage.taskCards.nth(i).textContent();
+ const text = await dagPage.taskRows.nth(i).textContent();
if (!text?.includes(operator)) return false;
}
@@ -119,11 +118,11 @@ test.describe("Dag Tasks Tab", () => {
await expect
.poll(
async () => {
- const count = await dagPage.taskCards.count();
+ const count = await dagPage.taskRows.count();
if (count === 0) return false;
for (let i = 0; i < count; i++) {
- const text = await dagPage.taskCards.nth(i).textContent();
+ const text = await dagPage.taskRows.nth(i).textContent();
if (!text?.includes(rule)) return false;
}
@@ -151,7 +150,7 @@ test.describe("Dag Tasks Tab", () => {
for (const retries of retriesOptions) {
await dagPage.filterByRetries(retries);
- await expect(dagPage.taskCards.first()).toBeVisible();
+ await expect(dagPage.taskRows.first()).toBeVisible();
await dagPage.navigateToDagTasks(testDagId);
}
});
@@ -160,7 +159,7 @@ test.describe("Dag Tasks Tab", () => {
await dagPage.navigateToDagTasks(testDagId);
- const firstCard = dagPage.taskCards.first();
+ const firstCard = dagPage.taskRows.first();
const taskLink = firstCard.locator("a").first();
await taskLink.click();