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 }) => {

Reply via email to