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 20f90a6e105 UI: Keep Dags list table rows on a single line (#74002)
20f90a6e105 is described below
commit 20f90a6e10522dcbfc389e58ab7ed8ae2a566499
Author: Yuseok Jo <[email protected]>
AuthorDate: Tue Oct 6 00:08:21 2026 +0900
UI: Keep Dags list table rows on a single line (#74002)
* UI: Keep Dags list table rows on a single line
* UI: Portal the schedule tooltip so the single-line cell does not affect it
---
.../ui/src/pages/DagsList/DagsList.test.tsx | 34 +++++++++++++
.../src/airflow/ui/src/pages/DagsList/DagsList.tsx | 39 +++++++++------
.../ui/src/pages/DagsList/Schedule.test.tsx | 58 ++++++++++++++++++++++
.../src/airflow/ui/src/pages/DagsList/Schedule.tsx | 2 +-
4 files changed, 118 insertions(+), 15 deletions(-)
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.test.tsx
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.test.tsx
index cc411788340..0605b8a6584 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.test.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.test.tsx
@@ -324,3 +324,37 @@ describe("Dag sorting", () => {
expect(screen.getByTestId("sort-index-dag_display_name")).toHaveTextContent("2");
});
});
+
+describe("Dags table", () => {
+ it.each([
+ "dag_display_name",
+ "timetable_description",
+ "next_dagrun",
+ "last_run_run_after",
+ "tags",
+ "team_name",
+ ])("keeps the %s cell on one line", async (columnId) => {
+ server.use(
+ http.get("/ui/config", () => HttpResponse.json({ multi_team: true,
page_size: 15 })),
+ http.get("/ui/dags", () =>
+ HttpResponse.json({
+ dags: [
+ {
+ ...successDag,
+ next_dagrun_run_after: "2025-01-14T00:00:00Z",
+ team_name: "team_a",
+ timetable_summary: "@daily",
+ },
+ ],
+ total_entries: 1,
+ }),
+ ),
+ );
+ localStorage.setItem(DAGS_LIST_DISPLAY_KEY, JSON.stringify("table"));
+ render(<AppWrapper initialEntries={["/dags"]} />);
+
+ const cell = await screen.findByTestId(`table-cell-${columnId}`);
+
+ await waitFor(() => expect(cell.firstElementChild).toHaveStyle({
whiteSpace: "nowrap" }));
+ });
+});
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
index b1da1380424..1b2af0c160b 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
@@ -120,7 +120,7 @@ const createColumns = (
{
accessorKey: "dag_display_name",
cell: ({ row: { original } }) => (
- <RouterLink fontWeight="bold" to={`/dags/${original.dag_id}`}>
+ <RouterLink fontWeight="bold" to={`/dags/${original.dag_id}`}
whiteSpace="nowrap">
{original.dag_display_name}
</RouterLink>
),
@@ -129,13 +129,15 @@ const createColumns = (
{
accessorKey: "timetable_description",
cell: ({ row: { original } }) => (
- <Schedule
- assetExpression={original.asset_expression}
- dagId={original.dag_id}
- timetableDescription={original.timetable_description}
- timetablePartitioned={original.timetable_partitioned}
- timetableSummary={original.timetable_summary}
- />
+ <Box whiteSpace="nowrap">
+ <Schedule
+ assetExpression={original.asset_expression}
+ dagId={original.dag_id}
+ timetableDescription={original.timetable_description}
+ timetablePartitioned={original.timetable_partitioned}
+ timetableSummary={original.timetable_summary}
+ />
+ </Box>
),
enableSorting: false,
header: () => translate("dagDetails.schedule"),
@@ -146,10 +148,12 @@ const createColumns = (
original.is_paused ? undefined : original.scheduling_state ===
"draining" ? (
<DrainingBadge />
) : Boolean(original.next_dagrun_run_after) ? (
- <DagRunInfo
- logicalDate={original.next_dagrun_logical_date}
- runAfter={original.next_dagrun_run_after as string}
- />
+ <Box whiteSpace="nowrap">
+ <DagRunInfo
+ logicalDate={original.next_dagrun_logical_date}
+ runAfter={original.next_dagrun_run_after as string}
+ />
+ </Box>
) : undefined,
header: () => translate("dagDetails.nextRun"),
},
@@ -160,6 +164,7 @@ const createColumns = (
<RouterLink
fontWeight="bold"
to={`/dags/${original.dag_id}/runs/${original.latest_dag_runs[0].run_id}`}
+ whiteSpace="nowrap"
>
<DagRunInfo
endDate={original.latest_dag_runs[0].end_date}
@@ -192,7 +197,11 @@ const createColumns = (
row: {
original: { tags },
},
- }) => <DagTags hideIcon tags={tags} />,
+ }) => (
+ <Box whiteSpace="nowrap">
+ <DagTags hideIcon tags={tags} />
+ </Box>
+ ),
enableSorting: false,
header: () => translate("dagDetails.tags"),
},
@@ -201,7 +210,9 @@ const createColumns = (
{
accessorKey: "team_name",
cell: ({ row: { original } }: { row: { original:
DAGWithLatestDagRunsResponse } }) => (
- <TeamName teamName={original.team_name} />
+ <Box whiteSpace="nowrap">
+ <TeamName teamName={original.team_name} />
+ </Box>
),
enableSorting: false,
header: () => translate("dagDetails.team"),
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/Schedule.test.tsx
b/airflow-core/src/airflow/ui/src/pages/DagsList/Schedule.test.tsx
new file mode 100644
index 00000000000..9731577b187
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/Schedule.test.tsx
@@ -0,0 +1,58 @@
+/*!
+ * 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/vitest";
+import { act, fireEvent, render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+import { BaseWrapper } from "src/utils/Wrapper";
+
+import { Schedule } from "./Schedule";
+
+describe("Schedule", () => {
+ it("renders the timetable description tooltip outside the cell, so the
cell's nowrap does not apply", async () => {
+ vi.useFakeTimers();
+
+ try {
+ const { container } = render(
+ <Schedule
+ assetExpression={undefined}
+ dagId="my_dag"
+ timetableDescription="At 09:00, Monday through Friday"
+ timetablePartitioned={false}
+ timetableSummary="0 9 * * 1-5"
+ />,
+ { wrapper: BaseWrapper },
+ );
+ const trigger = screen.getByText("0 9 * * 1-5");
+
+ await act(async () => {
+ fireEvent.focus(trigger);
+ fireEvent.pointerEnter(trigger);
+ await vi.advanceTimersByTimeAsync(500);
+ });
+
+ const tooltip = screen.getByRole("tooltip");
+
+ expect(tooltip).toHaveTextContent("At 09:00, Monday through Friday");
+ expect(container).not.toContainElement(tooltip);
+ } finally {
+ vi.useRealTimers();
+ }
+ });
+});
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/Schedule.tsx
b/airflow-core/src/airflow/ui/src/pages/DagsList/Schedule.tsx
index dc8476a51fd..af942c5c1c4 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/Schedule.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/Schedule.tsx
@@ -52,7 +52,7 @@ export const Schedule = ({
timetableSummary={timetableSummary}
/>
) : (
- <Tooltip content={timetableDescription}>
+ <Tooltip content={timetableDescription} portalled>
<Text fontSize="sm">
<FiCalendar style={{ display: "inline" }} /> {timetableSummary}
</Text>