diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.test.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.test.tsx new file mode 100644 index 0000000000000..31ddb7e98f30d --- /dev/null +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.test.tsx @@ -0,0 +1,114 @@ +/*! + * 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 "@testing-library/jest-dom"; +import { render, screen } from "@testing-library/react"; +import { MemoryRouter, Route, Routes } from "react-router-dom"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import type { LightGridTaskInstanceSummary } from "openapi/requests/types.gen"; +import { TimezoneProvider } from "src/context/timezone"; +import { BaseWrapper } from "src/utils/Wrapper"; + +import { GridTI } from "./GridTI"; +import { SELECTED_TASK_OUTLINE_COLOR } from "./constants"; + +const colorModeMock = vi.fn<() => { colorMode: "dark" | "light" | undefined }>(); + +vi.mock("src/context/colorMode", () => ({ + useColorMode: () => colorModeMock(), +})); + +const taskInstance: LightGridTaskInstanceSummary = { + child_states: null, + max_end_date: null, + min_start_date: null, + state: "success", + task_display_name: "selected_task", + task_id: "selected_task", +}; + +const SELECTED_RUN_ID = "manual__2026-04-21T00:00:00+00:00"; + +const renderGridTI = (route: string, taskId = "selected_task", runId = SELECTED_RUN_ID) => + render( + + + + + + } + path="/dags/:dagId/runs/:runId/tasks/:taskId" + /> + + + + , + ); + +describe("GridTI", () => { + beforeEach(() => { + colorModeMock.mockReturnValue({ colorMode: "light" }); + }); + + it("marks the selected task square", () => { + renderGridTI(`/dags/example_dag/runs/${SELECTED_RUN_ID}/tasks/selected_task`); + + expect(screen.getByTestId("task-state-badge")).toHaveAttribute("data-selected", "true"); + expect(screen.getByTestId("task-state-badge").closest("[data-task-id='selected_task']")).toHaveAttribute( + "data-selected", + "true", + ); + }); + + it("uses a lighter outline for the selected task square in dark mode", () => { + expect(SELECTED_TASK_OUTLINE_COLOR.dark).toBe("brand.contrast"); + }); + + it("does not mark another task square as selected", () => { + renderGridTI(`/dags/example_dag/runs/${SELECTED_RUN_ID}/tasks/selected_task`, "other_task"); + + expect(screen.getByTestId("task-state-badge")).not.toHaveAttribute("data-selected"); + expect(screen.getByTestId("task-state-badge").closest("[data-task-id='other_task']")).toHaveAttribute( + "data-selected", + "false", + ); + }); + + it("keeps the task row selected without marking the same task square in another Dag run as selected", () => { + renderGridTI( + `/dags/example_dag/runs/${SELECTED_RUN_ID}/tasks/selected_task`, + "selected_task", + "other_run", + ); + + expect(screen.getByTestId("task-state-badge")).not.toHaveAttribute("data-selected"); + expect(screen.getByTestId("task-state-badge").closest("[data-task-id='selected_task']")).toHaveAttribute( + "data-selected", + "true", + ); + }); +}); diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.tsx index 2a142cf98b142..4f575b8ee0ec2 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/GridTI.tsx @@ -22,9 +22,10 @@ import { Link, useLocation, useParams, useSearchParams } from "react-router-dom" import type { LightGridTaskInstanceSummary } from "openapi/requests/types.gen"; import { StateIcon } from "src/components/StateIcon"; import TaskInstanceTooltip from "src/components/TaskInstanceTooltip"; +import { useColorMode } from "src/context/colorMode"; import { buildTaskInstanceUrl } from "src/utils/links"; -import { NOTE_GRADIENT } from "./constants"; +import { NOTE_GRADIENT, SELECTED_TASK_OUTLINE_COLOR } from "./constants"; type Props = { readonly dagId: string; @@ -48,7 +49,8 @@ export const GridTI = ({ runId, taskId, }: Props) => { - const { groupId: selectedGroupId, taskId: selectedTaskId } = useParams(); + const { groupId: selectedGroupId, runId: selectedRunId, taskId: selectedTaskId } = useParams(); + const { colorMode = "light" } = useColorMode(); const location = useLocation(); const [searchParams] = useSearchParams(); @@ -68,14 +70,16 @@ export const GridTI = ({ searchParams.delete("try_number"); const redirectionSearch = searchParams.toString(); - const isSelected = selectedTaskId === taskId || selectedGroupId === taskId; + const isSelectedRow = selectedTaskId === taskId || selectedGroupId === taskId; + const isSelectedTaskInstance = selectedRunId === runId && isSelectedRow; + const selectedOutlineColor = SELECTED_TASK_OUTLINE_COLOR[colorMode]; return ( diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/constants.ts b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/constants.ts index fa2f987fffbc3..1cc4d00019bea 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/constants.ts +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/constants.ts @@ -41,6 +41,10 @@ export const BUNDLE_VERSION_INDICATOR_LEFT = -2; // Position from left for bundl export const BUNDLE_VERSION_ICON_SIZE = 15; // Size of the git commit icon export const DAG_VERSION_INDICATOR_HEIGHT = 104; // Height of the vertical line indicator export const VERSION_INDICATOR_Z_INDEX = 1; // Z-index for version indicators +export const SELECTED_TASK_OUTLINE_COLOR = { + dark: "brand.contrast", + light: "brand.solid", +} as const; // Render a gradient to indicate a saved note on a Dag run or task instance export const NOTE_GRADIENT =