This is an automated email from the ASF dual-hosted git repository.
choo121600 pushed a commit to branch v3-4-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-4-test by this push:
new d8c8c329d20 [v3-4-test] Fix Dag version diff max-changes border, limit
wording and duplicate count (#74818) (#74886)
d8c8c329d20 is described below
commit d8c8c329d201d8eebb8574bfda4bcc943d7e3aa0
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Sat Oct 17 09:05:37 2026 +0900
[v3-4-test] Fix Dag version diff max-changes border, limit wording and
duplicate count (#74818) (#74886)
---
.../src/airflow/ui/public/i18n/locales/ca/dag.json | 4 ---
.../src/airflow/ui/public/i18n/locales/de/dag.json | 4 ---
.../src/airflow/ui/public/i18n/locales/el/dag.json | 4 ---
.../src/airflow/ui/public/i18n/locales/en/dag.json | 11 +++---
.../src/airflow/ui/public/i18n/locales/fr/dag.json | 4 ---
.../src/airflow/ui/public/i18n/locales/ko/dag.json | 4 ---
.../src/airflow/ui/public/i18n/locales/pl/dag.json | 4 ---
.../airflow/ui/public/i18n/locales/zh-TW/dag.json | 4 ---
.../ui/src/pages/Dag/Versions/VersionDiff.test.tsx | 30 +++++++++++++---
.../ui/src/pages/Dag/Versions/VersionDiff.tsx | 41 ++++++++++++++++++----
.../ui/src/pages/Dag/Versions/Versions.test.tsx | 6 ++++
.../airflow/ui/src/pages/Dag/Versions/Versions.tsx | 9 +++++
12 files changed, 83 insertions(+), 42 deletions(-)
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ca/dag.json
b/airflow-core/src/airflow/ui/public/i18n/locales/ca/dag.json
index 455e6c134ea..8f8d87e3cfa 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/ca/dag.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/ca/dag.json
@@ -303,10 +303,6 @@
"summary_one": "{{count, number}} canvi",
"summary_other": "{{count, number}} canvis",
"target": "Comparar amb",
- "truncated": {
- "description": "S'han omès els canvis de les rutes no mostrades per
respectar el límit, de manera que el recompte és un mínim.",
- "title": "No s'han retornat tots els canvis"
- },
"unavailable": {
"description": "No s'han pogut comparar aquestes versions. Motiu:",
"title": "Comparació no disponible",
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/de/dag.json
b/airflow-core/src/airflow/ui/public/i18n/locales/de/dag.json
index bb59dd2f30b..5ecad36f4ad 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/de/dag.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/de/dag.json
@@ -303,10 +303,6 @@
"summary_one": "{{count, number}} Änderung",
"summary_other": "{{count, number}} Änderungen",
"target": "Vergleichen mit",
- "truncated": {
- "description": "Einige Änderungen werden nicht gezeigt um unter dem
Limit zu bleiben, die angezeigte Anzahl ist ein Minimum an Änderungen.",
- "title": "Einige Änderungen wurden nicht angezeigt"
- },
"unavailable": {
"description": "Diese Versionen können nicht verglichen werden. Grund:",
"title": "Vergleich nicht verfügbar",
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/el/dag.json
b/airflow-core/src/airflow/ui/public/i18n/locales/el/dag.json
index 2b00a6442cd..573ab07e7f8 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/el/dag.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/el/dag.json
@@ -303,10 +303,6 @@
"summary_one": "{{count, number}} αλλαγή",
"summary_other": "{{count, number}} αλλαγές",
"target": "Σύγκριση με",
- "truncated": {
- "description": "Οι αλλαγές σε διαδρομές που δεν εμφανίζονται
παραλείφθηκαν για να τηρηθεί το όριο, επομένως το πλήθος είναι ένας μικρότερος
αριθμός ορίου.",
- "title": "Ορισμένες αλλαγές δεν επιστράφηκαν"
- },
"unavailable": {
"description": "Δεν ήταν δυνατή η σύγκριση αυτών των εκδόσεων. Αιτία:",
"title": "Η σύγκριση δεν είναι διαθέσιμη",
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json
b/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json
index a30b6f8b5c6..a5d0da02be8 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json
@@ -334,6 +334,11 @@
"provenance": "Provenance",
"unknown": "Unknown"
},
+ "limitReached": {
+ "description": "There are more changes than the Max changes limit
allows. Raise the limit to see the rest.",
+ "descriptionDefault": "There are more changes than the default limit
allows. Set Max changes higher to see the rest.",
+ "title": "Showing only the first {{limit, number}} changes"
+ },
"maxChanges": "Max changes",
"maxChangesDefault": "Default",
"noChanges": "These versions hold the same state.",
@@ -350,11 +355,9 @@
"selectPrompt": "Select two versions to see what changed between them.",
"summary_one": "{{count, number}} change",
"summary_other": "{{count, number}} changes",
+ "summaryAtLeast_one": "At least {{count, number}} change",
+ "summaryAtLeast_other": "At least {{count, number}} changes",
"target": "Compare to",
- "truncated": {
- "description": "Changes at paths not shown were dropped to stay within
the limit, so the count is a lower bound.",
- "title": "Some changes were not returned"
- },
"unavailable": {
"description": "These versions could not be compared. Reason:",
"title": "Comparison unavailable",
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/fr/dag.json
b/airflow-core/src/airflow/ui/public/i18n/locales/fr/dag.json
index 48e2111abef..275a6af3dc0 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/fr/dag.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/fr/dag.json
@@ -314,10 +314,6 @@
"summary_one": "{{count, number}} changement",
"summary_other": "{{count, number}} changements",
"target": "Comparer avec",
- "truncated": {
- "description": "Les changements aux chemins non affichés ont été
supprimés pour rester dans la limite, donc le nombre est une borne inférieure.",
- "title": "Certains changements n'ont pas été renvoyés"
- },
"unavailable": {
"description": "Ces versions n'ont pas pu être comparées. Raison :",
"title": "Comparaison indisponible",
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/ko/dag.json
b/airflow-core/src/airflow/ui/public/i18n/locales/ko/dag.json
index ee202b4e3fa..3dc1cdc451b 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/ko/dag.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/ko/dag.json
@@ -350,10 +350,6 @@
"summary_one": "변경 {{count, number}}개",
"summary_other": "변경 {{count, number}}개",
"target": "비교 대상",
- "truncated": {
- "description": "한도를 넘지 않도록 일부 경로의 변경이 제외되었으므로, 표시된 변경 수는 실제보다 적을 수
있습니다.",
- "title": "일부 변경 사항이 반환되지 않았습니다"
- },
"unavailable": {
"description": "두 버전을 비교할 수 없습니다. 이유:",
"title": "비교할 수 없음",
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/pl/dag.json
b/airflow-core/src/airflow/ui/public/i18n/locales/pl/dag.json
index 7376278aa54..87dbcf28ef8 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/pl/dag.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/pl/dag.json
@@ -325,10 +325,6 @@
"summary_one": "{{count, number}} zmiana",
"summary_other": "{{count, number}} zmiany",
"target": "Porównaj z",
- "truncated": {
- "description": "Zmiany w niepokazanych ścieżkach zostały pominięte, aby
zmieścić się w limicie, więc liczba zmian jest dolnym oszacowaniem.",
- "title": "Nie zwrócono niektórych zmian"
- },
"unavailable": {
"description": "Nie udało się porównać tych wersji. Przyczyna:",
"title": "Porównanie niedostępne",
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/dag.json
b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/dag.json
index 7ac9a24163c..94b7d8704f2 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/dag.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/zh-TW/dag.json
@@ -350,10 +350,6 @@
"summary_one": "1 項變更",
"summary_other": "{{count, number}} 項變更",
"target": "比較對象",
- "truncated": {
- "description": "為維持在上限內,未顯示的路徑上的變更已被捨棄,因此此數量僅為下限。",
- "title": "部分變更未回傳"
- },
"unavailable": {
"description": "無法比較這些版本。原因:",
"title": "無法比較",
diff --git
a/airflow-core/src/airflow/ui/src/pages/Dag/Versions/VersionDiff.test.tsx
b/airflow-core/src/airflow/ui/src/pages/Dag/Versions/VersionDiff.test.tsx
index d754220454a..5a97d7c5a6e 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Versions/VersionDiff.test.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Versions/VersionDiff.test.tsx
@@ -79,10 +79,10 @@ const authorized: DagVersionDiffResponse = {
values_status: "available",
};
-const renderDiff = (diff: DagVersionDiffResponse) =>
+const renderDiff = (diff: DagVersionDiffResponse, maxChanges?: number) =>
render(
<Wrapper>
- <VersionDiff baseVersionNumber={1} diff={diff} targetVersionNumber={2} />
+ <VersionDiff baseVersionNumber={1} diff={diff} maxChanges={maxChanges}
targetVersionNumber={2} />
</Wrapper>,
);
@@ -154,10 +154,32 @@ describe("VersionDiff", () => {
expect(screen.getByText("bob")).toBeInTheDocument();
});
- it("warns that a truncated count is a lower bound", () => {
+ it("says a truncated count is a lower bound and points at the limit that was
set", () => {
+ renderDiff({ ...redacted, truncated: true }, 10);
+
+ expect(screen.getByText("versions.summaryAtLeast")).toBeInTheDocument();
+
expect(screen.getByText("versions.limitReached.title")).toBeInTheDocument();
+
expect(screen.getByText("versions.limitReached.description")).toBeInTheDocument();
+ });
+
+ it("names the default limit when no bound was set", () => {
renderDiff({ ...redacted, truncated: true });
-
expect(screen.getByText("versions.truncated.description")).toBeInTheDocument();
+
expect(screen.getByText("versions.limitReached.descriptionDefault")).toBeInTheDocument();
+ });
+
+ it("counts changes once, in the summary, not again above the table", () => {
+ renderDiff(redacted);
+
+ expect(screen.getByText("versions.summary")).toBeInTheDocument();
+
expect(screen.queryByText(/dag:versions\.record|versions\.record/u)).not.toBeInTheDocument();
+ });
+
+ it("gives an exact count when nothing was dropped", () => {
+ renderDiff(redacted);
+
+ expect(screen.getByText("versions.summary")).toBeInTheDocument();
+
expect(screen.queryByText("versions.limitReached.title")).not.toBeInTheDocument();
});
it("marks the side an added change does not have", () => {
diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Versions/VersionDiff.tsx
b/airflow-core/src/airflow/ui/src/pages/Dag/Versions/VersionDiff.tsx
index c09d019ceec..6aefa219149 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Versions/VersionDiff.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Versions/VersionDiff.tsx
@@ -31,9 +31,15 @@ import type { MetaColumn } from
"src/components/DataTable/types";
type VersionDiffProps = {
readonly baseVersionNumber: number;
readonly diff: DagVersionDiffResponse;
+ /** The bound the request asked for; undefined means the endpoint's default
applied. */
+ readonly maxChanges?: number;
readonly targetVersionNumber: number;
};
+// Mirrors DEFAULT_MAX_CHANGES on the diff endpoint, so the truncation notice
can name the limit
+// that cut the comparison short when the caller left the field empty.
+const DEFAULT_MAX_CHANGES = 500;
+
const IMPACT_COLORS: Record<string, string> = {
authorization: "orange",
execution: "red",
@@ -120,7 +126,12 @@ const buildColumns = (translate: TFunction<"dag">,
valuesShown: boolean): Change
...(valuesShown ? buildValueColumns(translate) : []),
];
-export const VersionDiff = ({ baseVersionNumber, diff, targetVersionNumber }:
VersionDiffProps) => {
+export const VersionDiff = ({
+ baseVersionNumber,
+ diff,
+ maxChanges,
+ targetVersionNumber,
+}: VersionDiffProps) => {
const { t: translate } = useTranslation("dag");
const valuesShown = diff.values_status === "available";
const schemaChanged = diff.serializer_versions.base !==
diff.serializer_versions.target;
@@ -151,7 +162,10 @@ export const VersionDiff = ({ baseVersionNumber, diff,
targetVersionNumber }: Ve
{translate("versions.heading", { base: baseVersionNumber, target:
targetVersionNumber })}
</Heading>
<Text color="fg.muted" fontSize="sm">
- {translate("versions.summary", { count: diff.total_changes })}
+ {/* A truncated count is a lower bound, so it says "at least"
rather than a bare number. */}
+ {translate(diff.truncated ? "versions.summaryAtLeast" :
"versions.summary", {
+ count: diff.total_changes,
+ })}
{/* Only worth a mention when the two versions were stored by
different serializers. */}
{schemaChanged
? ` · ${translate("versions.schemaChange", {
@@ -182,18 +196,33 @@ export const VersionDiff = ({ baseVersionNumber, diff,
targetVersionNumber }: Ve
</Flex>
{diff.truncated ? (
- <Alert.Root mb={2} status="warning"
title={translate("versions.truncated.title")}>
-
<Alert.Description>{translate("versions.truncated.description")}</Alert.Description>
+ // The title is a child, not a `title` prop: Chakra's Alert.Root
passes that through as the
+ // HTML attribute, so it only ever showed as a hover tooltip.
+ <Alert.Root mb={2} status="warning">
+ <Alert.Indicator />
+ <Alert.Content>
+ <Alert.Title>
+ {translate("versions.limitReached.title", { limit: maxChanges ??
DEFAULT_MAX_CHANGES })}
+ </Alert.Title>
+ <Alert.Description>
+ {translate(
+ maxChanges === undefined
+ ? "versions.limitReached.descriptionDefault"
+ : "versions.limitReached.description",
+ )}
+ </Alert.Description>
+ </Alert.Content>
</Alert.Root>
) : undefined}
<DataTable
columns={buildColumns(translate, valuesShown)}
data={diff.changes}
+ // The summary above already counts the changes. A second "N Records"
count only invites a
+ // comparison that does not hold: a redacted record stands for every
change sharing its path.
+ hideRowCountHeading
modelName="dag:versions.record"
noRowsMessage={translate("versions.noChanges")}
- // Records, not underlying changes: a redacted record stands for every
change sharing its
- // path, which is why this can read lower than the count in the
summary above.
total={diff.changes.length}
/>
</Box>
diff --git
a/airflow-core/src/airflow/ui/src/pages/Dag/Versions/Versions.test.tsx
b/airflow-core/src/airflow/ui/src/pages/Dag/Versions/Versions.test.tsx
index 4925c8c3ebe..912197764b8 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Versions/Versions.test.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Versions/Versions.test.tsx
@@ -224,4 +224,10 @@ describe("Versions", () => {
await waitFor(() =>
expect(screen.getByTestId("search")).toHaveTextContent("max_changes=42"));
});
+
+ it("does not flag the empty max changes field as invalid", () => {
+ renderVersions("?base_version_number=1&target_version_number=3");
+
+
expect(screen.getByLabelText("versions.maxChanges")).not.toHaveAttribute("aria-invalid");
+ });
});
diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Versions/Versions.tsx
b/airflow-core/src/airflow/ui/src/pages/Dag/Versions/Versions.tsx
index b59c693f28f..f2f35151b27 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Versions/Versions.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Versions/Versions.tsx
@@ -75,6 +75,7 @@ const SelectedVersionsDiff = ({
<VersionDiff
baseVersionNumber={baseVersionNumber}
diff={data}
+ maxChanges={maxChanges}
targetVersionNumber={targetVersionNumber}
/>
)}
@@ -104,6 +105,13 @@ export const Versions = () => {
// bound the endpoint accepts.
const [maxChangesDraft, setMaxChangesDraft] = useState(maxChanges ===
undefined ? "" : `${maxChanges}`);
+ // Explicit because the number input otherwise flags an empty field as out
of range (an empty
+ // value parses to NaN), which drew an error border around the field's
normal, unset state.
+ const draftNumber = Number(maxChangesDraft);
+ const maxChangesInvalid =
+ maxChangesDraft !== "" &&
+ (!Number.isInteger(draftNumber) || draftNumber < 1 || draftNumber >
MAX_CHANGES_LIMIT);
+
const updateParam = (key: string, value: string) =>
setSearchParams(
(prev) => {
@@ -149,6 +157,7 @@ export const Versions = () => {
<VStack alignItems="flex-start" gap={1}>
<Text fontSize="xs">{translate("versions.maxChanges")}</Text>
<NumberInputRoot
+ invalid={maxChangesInvalid}
max={MAX_CHANGES_LIMIT}
min={1}
onValueChange={({ value }) => {