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) };