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 e9d54fcc998 Fix mapped task instance links without start date (#68194)
e9d54fcc998 is described below

commit e9d54fcc998e875957479e55349e54b910102987
Author: Revanth <[email protected]>
AuthorDate: Mon Jun 8 09:43:01 2026 -0500

    Fix mapped task instance links without start date (#68194)
    
    * Fix mapped task instance links without start date
    
    * update comment
---
 .../src/pages/TaskInstances/TaskInstances.test.tsx | 168 +++++++++++++++++++++
 .../ui/src/pages/TaskInstances/TaskInstances.tsx   |   9 ++
 2 files changed, 177 insertions(+)

diff --git 
a/airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstances.test.tsx 
b/airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstances.test.tsx
new file mode 100644
index 00000000000..1bf4f98f496
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstances.test.tsx
@@ -0,0 +1,168 @@
+/*!
+ * 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 { render, screen } from "@testing-library/react";
+import type { ReactNode } from "react";
+import type * as ReactI18Next from "react-i18next";
+import type * as ReactRouterDom from "react-router-dom";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import type * as OpenapiQueries from "openapi/queries";
+import type { TaskInstanceResponse } from "openapi/requests/types.gen";
+import { Wrapper } from "src/utils/Wrapper";
+
+import { TaskInstances } from "./TaskInstances";
+
+let mockParams: Record<string, string | undefined> = {
+  dagId: "example_dag",
+  runId: "manual__2026-06-07T00:00:00+00:00",
+  taskId: "mapped_task",
+};
+let mockSearchParams = new URLSearchParams();
+
+vi.mock("react-i18next", async (importOriginal) => {
+  const actual = await importOriginal<typeof ReactI18Next>();
+
+  return {
+    ...actual,
+    useTranslation: () => ({
+      // eslint-disable-next-line id-length
+      t: (key: string) => key,
+    }),
+  };
+});
+
+vi.mock("react-router-dom", async (importOriginal) => {
+  const actual = await importOriginal<typeof ReactRouterDom>();
+
+  return {
+    ...actual,
+    useParams: () => mockParams,
+    useSearchParams: () => [mockSearchParams, vi.fn()] as const,
+  };
+});
+
+vi.mock("openapi/queries", async (importOriginal) => {
+  const actual = await importOriginal<typeof OpenapiQueries>();
+
+  return {
+    ...actual,
+    useTaskInstanceServiceGetTaskInstances: vi.fn(),
+  };
+});
+
+vi.mock("./TaskInstancesFilter", () => ({
+  TaskInstancesFilter: () => null,
+}));
+
+vi.mock("src/components/DataTable", () => ({
+  DataTable: ({
+    columns,
+    data,
+  }: {
+    readonly columns: ReadonlyArray<{
+      accessorKey?: string;
+      cell?: (props: { row: { original: TaskInstanceResponse } }) => ReactNode;
+    }>;
+    readonly data: ReadonlyArray<TaskInstanceResponse>;
+  }) => {
+    const renderedMapIndexColumn = columns.find((column) => column.accessorKey 
=== "rendered_map_index");
+
+    return (
+      <table>
+        <tbody>
+          {data.map((taskInstance) => (
+            <tr key={`${taskInstance.task_id}-${taskInstance.map_index}`}>
+              <td>{renderedMapIndexColumn?.cell?.({ row: { original: 
taskInstance } })}</td>
+            </tr>
+          ))}
+        </tbody>
+      </table>
+    );
+  },
+}));
+
+const { useTaskInstanceServiceGetTaskInstances } = await 
import("openapi/queries");
+
+const getTaskInstancesResponse = (taskInstances: Array<TaskInstanceResponse>) 
=>
+  ({
+    data: {
+      task_instances: taskInstances,
+    },
+    error: null,
+    isLoading: false,
+  }) as ReturnType<typeof useTaskInstanceServiceGetTaskInstances>;
+
+const mappedTaskInstance = {
+  dag_display_name: "example_dag",
+  dag_id: "example_dag",
+  dag_run_id: "manual__2026-06-07T00:00:00+00:00",
+  map_index: 1,
+  rendered_map_index: "1",
+  start_date: null,
+  state: "queued",
+  task_display_name: "mapped_task",
+  task_id: "mapped_task",
+} as TaskInstanceResponse;
+
+describe("TaskInstances", () => {
+  beforeEach(() => {
+    mockParams = {
+      dagId: "example_dag",
+      runId: "manual__2026-06-07T00:00:00+00:00",
+      taskId: "mapped_task",
+    };
+    mockSearchParams = new URLSearchParams();
+  });
+
+  it.each([
+    {
+      description: "with a selected task but no run",
+      params: {
+        dagId: "example_dag",
+        taskId: "mapped_task",
+      },
+    },
+    {
+      description: "without a selected task or run",
+      params: {},
+    },
+  ])("links mapped task instances from the map index column $description", ({ 
params }) => {
+    mockParams = params;
+    vi.mocked(useTaskInstanceServiceGetTaskInstances).mockReturnValue(
+      getTaskInstancesResponse([mappedTaskInstance]),
+    );
+
+    render(<TaskInstances />, { wrapper: Wrapper });
+
+    expect(screen.getByRole("link", { name: "1" })).toHaveAttribute(
+      "href",
+      
"/dags/example_dag/runs/manual__2026-06-07T00:00:00+00:00/tasks/mapped_task/mapped/1",
+    );
+  });
+
+  it("does not link non-mapped task instances from the map index column", () 
=> {
+    vi.mocked(useTaskInstanceServiceGetTaskInstances).mockReturnValue(
+      getTaskInstancesResponse([{ ...mappedTaskInstance, map_index: -1, 
rendered_map_index: null }]),
+    );
+
+    render(<TaskInstances />, { wrapper: Wrapper });
+
+    expect(screen.queryByRole("link")).not.toBeInTheDocument();
+  });
+});
diff --git 
a/airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstances.tsx 
b/airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstances.tsx
index b1a8a4aa9e4..895e87af92d 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstances.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstances.tsx
@@ -160,6 +160,15 @@ const taskInstanceColumns = ({
       ]),
   {
     accessorKey: "rendered_map_index",
+    // Map index remains visible before a mapped task instance starts.
+    cell: ({ row: { original } }) =>
+      original.map_index >= 0 ? (
+        <RouterLink fontWeight="bold" to={getTaskInstanceLink(original)}>
+          {original.rendered_map_index ?? original.map_index}
+        </RouterLink>
+      ) : (
+        original.rendered_map_index
+      ),
     header: translate("mapIndex"),
   },
   {

Reply via email to