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