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 0d2f7490e53 UI: Keep Dag card columns aligned across cards (#76004)
0d2f7490e53 is described below

commit 0d2f7490e5373b0e98d266df52e576f38db7476b
Author: Yuseok Jo <[email protected]>
AuthorDate: Sun Nov 1 20:25:32 2026 +0900

    UI: Keep Dag card columns aligned across cards (#76004)
---
 .../src/airflow/ui/src/pages/DagsList/DagCard.tsx  | 14 +++++++-----
 .../src/pages/DagsList/DagRunStateCounts.test.tsx  |  5 +++++
 .../ui/src/pages/DagsList/DagRunStateCounts.tsx    |  2 +-
 .../src/airflow/ui/src/pages/DagsList/DagsList.tsx |  3 ++-
 .../airflow/ui/src/pages/DagsList/RecentRuns.tsx   |  6 +++--
 .../ui/src/pages/DagsList/recentRunsLayout.ts      | 26 ++++++++++++++++++++++
 6 files changed, 46 insertions(+), 10 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx
index c9d276e44e0..7aaf296f621 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx
@@ -39,6 +39,7 @@ import { useRevealedDagCards } from "./DagsListCountsContext";
 import { RecentRuns } from "./RecentRuns";
 import { RecentTaskStateCounts } from "./RecentTaskStateCounts";
 import { Schedule } from "./Schedule";
+import { RECENT_RUNS_WIDTH } from "./recentRunsLayout";
 
 type Props = {
   readonly dag: DAGWithLatestDagRunsResponse;
@@ -58,6 +59,7 @@ export const DagCard = ({
   const { t: translate } = useTranslation(["common", "dag"]);
   const [latestRun] = dag.latest_dag_runs;
   const multiTeamEnabled = Boolean(useConfig("multi_team"));
+  const infoColumns = multiTeamEnabled ? 4 : 3;
   const revealedKeys = useRevealedDagCards();
   const { isNearViewport, ref, showContent } = 
useNearViewport<HTMLDivElement>({
     revealedKeys,
@@ -92,8 +94,8 @@ export const DagCard = ({
         gap={1}
         px={3}
         py={2}
-        templateColumns={multiTeamEnabled ? "repeat(5, 1fr)" : "repeat(4, 
1fr)"}
-        templateRows="auto auto"
+        templateColumns={`repeat(${infoColumns}, 1fr) auto`}
+        templateRows="1fr auto"
       >
         <GridItem gridColumn={1} gridRow={1}>
           <Stat data-testid="schedule" 
label={translate("dagDetails.schedule")}>
@@ -147,15 +149,15 @@ export const DagCard = ({
         <GridItem
           alignItems="flex-end"
           display="flex"
-          gridColumn={multiTeamEnabled ? 5 : 4}
+          gridColumn={infoColumns + 1}
           gridRow="1 / 3"
           justifyContent="flex-end"
         >
-          <Box minHeight="65px">
+          <Box minHeight="65px" width={RECENT_RUNS_WIDTH}>
             {isNearViewport ? <RecentRuns latestRuns={dag.latest_dag_runs} /> 
: undefined}
           </Box>
         </GridItem>
-        <GridItem alignSelf="end" gridColumn={1} gridRow={2}>
+        <GridItem alignSelf="start" gridColumn="1 / 3" gridRow={2}>
           <Box minHeight="22px">
             {isNearViewport ? (
               <DagRunStateCounts
@@ -168,7 +170,7 @@ export const DagCard = ({
           </Box>
         </GridItem>
         {recentTasks.show ? (
-          <GridItem alignSelf="end" gridColumn="2 / 4" gridRow={2}>
+          <GridItem alignSelf="start" gridColumn={`3 / ${infoColumns + 1}`} 
gridRow={2}>
             <Box minHeight="22px">
               {isNearViewport ? (
                 <RecentTaskStateCounts
diff --git 
a/airflow-core/src/airflow/ui/src/pages/DagsList/DagRunStateCounts.test.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagRunStateCounts.test.tsx
index 7dd58a69bf0..d41ffe1eb1e 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagRunStateCounts.test.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagRunStateCounts.test.tsx
@@ -98,6 +98,11 @@ describe("DagRunStateCounts", () => {
     
expect(screen.getByTestId("run-state-count-failed-my_dag")).toHaveTextContent("2");
   });
 
+  it("wraps the badges outside the compact table cell", () => {
+    renderCounts({ failed: 0, queued: 0, running: 0, success: 1000 }, { 
stateCountLimit: 1000 });
+    expect(screen.getByTestId("run-state-counts-my_dag")).toHaveStyle({ 
flexWrap: "wrap" });
+  });
+
   it("suffixes counts that reached the cap with '+'", () => {
     renderCounts({ failed: 5, queued: 0, running: 1, success: 1000 }, { 
stateCountLimit: 1000 });
     
expect(screen.getByTestId("run-state-count-success-my_dag")).toHaveTextContent("1,000+");
diff --git 
a/airflow-core/src/airflow/ui/src/pages/DagsList/DagRunStateCounts.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagRunStateCounts.tsx
index 30da8d7ff21..113b64fb0b2 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagRunStateCounts.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagRunStateCounts.tsx
@@ -79,7 +79,7 @@ export const DagRunStateCounts = ({ compact = false, counts, 
dagId, isLoading, s
   // cards doesn't mount hundreds of tooltip state machines (same pattern as 
RecentRuns).
   return (
     <Tooltip.Root>
-      <HStack data-testid={`run-state-counts-${dagId}`} gap={gap}>
+      <HStack data-testid={`run-state-counts-${dagId}`} gap={gap} 
wrap={compact ? "nowrap" : "wrap"}>
         {DISPLAYED_STATES.map((state) => {
           const { formattedCount, isZero, label } = describeState(state);
 
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 7b209354ed6..83464a0e077 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
@@ -60,6 +60,7 @@ import { DagsFilters } from "./DagsFilters";
 import { createColumns, getRowKey } from "./DagsListColumns";
 import { DagsListCountsProvider, useDagsListCounts, type RunStateCounts } from 
"./DagsListCountsContext";
 import { SortSelect } from "./SortSelect";
+import { MAX_RECENT_RUNS } from "./recentRunsLayout";
 
 const {
   DAG_BUNDLE,
@@ -143,7 +144,7 @@ export const DagsList = () => {
 
   const [searchParams, setSearchParams] = useSearchParams();
   const [display, setDisplay] = useLocalStorage<"card" | 
"table">(DAGS_LIST_DISPLAY_KEY, "card");
-  const dagRunsLimit = display === "card" ? 14 : 1;
+  const dagRunsLimit = display === "card" ? MAX_RECENT_RUNS : 1;
 
   const multiTeamEnabled = Boolean(useConfig("multi_team"));
 
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/RecentRuns.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/RecentRuns.tsx
index c31601b92fc..0d474b2cabe 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/RecentRuns.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/RecentRuns.tsx
@@ -29,6 +29,8 @@ import Time from "src/components/Time";
 
 import { useDurationFormat } from "src/utils";
 
+import { BAR_GAP, BAR_WIDTH } from "./recentRunsLayout";
+
 dayjs.extend(duration);
 
 const BAR_HEIGHT = 65;
@@ -93,7 +95,7 @@ export const RecentRuns = ({
       <Flex
         alignItems="flex-end"
         flexDirection="row-reverse"
-        gap={[0.5, 0.5, 0.5, 1]}
+        gap={{ base: `${BAR_GAP.base}px`, lg: `${BAR_GAP.lg}px` }}
         minHeight={`${BAR_HEIGHT}px`}
         pb={1}
       >
@@ -109,7 +111,7 @@ export const RecentRuns = ({
                 height={`${run.duration === null ? 1 : (run.duration / max) * 
BAR_HEIGHT}px`}
                 justifyContent="flex-end"
                 minHeight="12px"
-                width="12px"
+                width={`${BAR_WIDTH}px`}
               >
                 <StateIcon color="white" state={run.state} />
               </Flex>
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/recentRunsLayout.ts 
b/airflow-core/src/airflow/ui/src/pages/DagsList/recentRunsLayout.ts
new file mode 100644
index 00000000000..43b448023b2
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/recentRunsLayout.ts
@@ -0,0 +1,26 @@
+/*!
+ * 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.
+ */
+export const BAR_WIDTH = 12;
+export const BAR_GAP = { base: 2, lg: 4 };
+
+export const MAX_RECENT_RUNS = 14;
+const getFullRowWidth = (gap: number) => `${MAX_RECENT_RUNS * BAR_WIDTH + 
(MAX_RECENT_RUNS - 1) * gap}px`;
+
+// A full row of bars, so a Dag card keeps its column widths whatever its run 
count.
+export const RECENT_RUNS_WIDTH = { base: getFullRowWidth(BAR_GAP.base), lg: 
getFullRowWidth(BAR_GAP.lg) };

Reply via email to