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 16df29a4513 UI: Highlight selected Grid task square (#70451)
16df29a4513 is described below

commit 16df29a4513fcb1d8bba3af3a4b3761418230310
Author: Dheeraj Turaga <[email protected]>
AuthorDate: Tue Aug 4 16:37:26 2026 -0500

    UI: Highlight selected Grid task square (#70451)
    
    * UI: Highlight selected Grid task square
    
    * UI: Restore selected Grid row highlight
---
 .../ui/src/layouts/Details/Grid/GridTI.test.tsx    | 114 +++++++++++++++++++++
 .../airflow/ui/src/layouts/Details/Grid/GridTI.tsx |  20 +++-
 .../ui/src/layouts/Details/Grid/constants.ts       |   4 +
 3 files changed, 133 insertions(+), 5 deletions(-)

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 00000000000..31ddb7e98f3
--- /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(
+    <BaseWrapper>
+      <TimezoneProvider>
+        <MemoryRouter initialEntries={[route]}>
+          <Routes>
+            <Route
+              element={
+                <GridTI
+                  dagId="example_dag"
+                  instance={{ ...taskInstance, task_id: taskId }}
+                  label={taskId}
+                  runId={runId}
+                  taskId={taskId}
+                />
+              }
+              path="/dags/:dagId/runs/:runId/tasks/:taskId"
+            />
+          </Routes>
+        </MemoryRouter>
+      </TimezoneProvider>
+    </BaseWrapper>,
+  );
+
+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 2a142cf98b1..4f575b8ee0e 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 (
     <Flex
       alignItems="center"
-      bg={isSelected ? "brand.emphasized" : undefined}
+      bg={isSelectedRow ? "brand.emphasized" : undefined}
       data-run-id={runId}
-      data-selected={isSelected}
+      data-selected={isSelectedRow}
       data-task-id={taskId}
       height="20px"
       id={`task-${taskId.replaceAll(".", "-")}`}
@@ -107,13 +111,19 @@ export const GridTI = ({
               alignItems="center"
               borderRadius={4}
               colorPalette={instance.state ?? "none"}
+              data-selected={isSelectedTaskInstance || undefined}
               data-testid="task-state-badge"
               display="flex"
               height="14px"
               justifyContent="center"
               minH={0}
+              outlineColor={isSelectedTaskInstance ? selectedOutlineColor : 
undefined}
+              outlineOffset="1px"
+              outlineStyle={isSelectedTaskInstance ? "solid" : undefined}
+              outlineWidth={isSelectedTaskInstance ? "2px" : undefined}
               p={0}
               style={hasNote ? { background: NOTE_GRADIENT } : undefined}
+              transition="outline-color 0.2s"
               variant="solid"
               width="14px"
             >
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 fa2f987fffb..1cc4d00019b 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 =

Reply via email to