This is an automated email from the ASF dual-hosted git repository.

vatsrahul1001 pushed a commit to branch v3-4-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit 9f79f221389faafc8c0ebd1892e42f18568a4c41
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Fri Oct 30 21:26:52 2026 +0530

    [v3-4-test] UI: Stop Recent Configurations flashing on the trigger form 
(#75991) (#75994)
    
    * UI: Stop Recent Configurations flashing on the trigger form
    
    The section decided to hide itself only once its runs had arrived, so every
    time the trigger form opened it first drew an empty, disabled select and 
then
    either filled it or removed it. The query does not start until the modal
    opens, which reports as pending rather than loading, so neither flag on its
    own tells you the runs are in hand.
    
    
    
    * UI: Lay out Recent Configurations like the rest of the trigger form
    
    Every other row in the form names itself in a column down the left and puts
    its control to the right. This one announced itself in small type above a
    control that ran the full width, so the eye had to change track in the 
middle
    of the form.
    
    
    
    * UI: Offer recent configurations when they come from cache
    
    Runs stay fresh for five minutes, so reopening the trigger form inside that
    window serves them from cache without fetching. Asking the query whether it
    had fetched since mounting read that as "not ready" and hid configurations
    that were already in hand.
    
    Nothing needs asking. The options are built from the runs alone, so they are
    empty both before the runs arrive and when none of them carried a config, 
and
    in either case there is nothing to show.
    
    
    
    ---------
    (cherry picked from commit 212fe09e1d0e384473b5ece2a2b1f66cff9e0a4b)
    
    Co-authored-by: Brent Bovenzi <[email protected]>
    Co-authored-by: Claude <[email protected]>
---
 .../TriggerDag/RecentConfigSelect.test.tsx         | 39 +++++++++--
 .../components/TriggerDag/RecentConfigSelect.tsx   | 79 ++++++++++++++--------
 .../components/TriggerDag/TriggerDAGForm.test.tsx  |  4 +-
 3 files changed, 87 insertions(+), 35 deletions(-)

diff --git 
a/airflow-core/src/airflow/ui/src/components/TriggerDag/RecentConfigSelect.test.tsx
 
b/airflow-core/src/airflow/ui/src/components/TriggerDag/RecentConfigSelect.test.tsx
index 09a39585e49..8bf0d9004f9 100644
--- 
a/airflow-core/src/airflow/ui/src/components/TriggerDag/RecentConfigSelect.test.tsx
+++ 
b/airflow-core/src/airflow/ui/src/components/TriggerDag/RecentConfigSelect.test.tsx
@@ -16,9 +16,9 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import "@testing-library/jest-dom";
+import "@testing-library/jest-dom/vitest";
 import { fireEvent, render, screen, waitFor } from "@testing-library/react";
-import { describe, expect, it, vi } from "vitest";
+import { beforeEach, describe, expect, it, vi } from "vitest";
 
 import { Wrapper } from "src/utils/Wrapper";
 
@@ -50,15 +50,22 @@ const buildRun = (overrides: Record<string, unknown>) => ({
 });
 
 let mockData: { dag_runs: Array<ReturnType<typeof buildRun>> } | undefined;
-const mockIsLoading = false;
+// Served from cache on a reopen: data is present without any fetch having 
happened since mount.
+let mockFetchedAfterMount = true;
 
 vi.mock("openapi/queries", () => ({
   useDagRunServiceGetDagRuns: vi.fn(() => ({
     data: mockData,
-    isLoading: mockIsLoading,
+    isFetchedAfterMount: mockFetchedAfterMount,
+    isFetching: false,
+    isSuccess: mockData !== undefined,
   })),
 }));
 
+beforeEach(() => {
+  mockFetchedAfterMount = true;
+});
+
 // Select.Content is portalled to document.body, outside the render container.
 const getItems = () => document.body.querySelectorAll(".chakra-select__item");
 
@@ -101,6 +108,30 @@ describe("RecentConfigSelect", () => {
     expect(container).toBeEmptyDOMElement();
   });
 
+  it("renders nothing while the runs are still loading", () => {
+    mockFetchedAfterMount = false;
+    mockData = undefined;
+
+    const { container } = render(<RecentConfigSelect dagId="test_dag" 
onSelectConf={vi.fn()} open />, {
+      wrapper: Wrapper,
+    });
+
+    expect(container).toBeEmptyDOMElement();
+  });
+
+  // Runs stay fresh for five minutes, so reopening the modal inside that 
window serves them from
+  // cache and nothing is fetched after this mount. The configs are still 
there to offer.
+  it("still offers configs when the runs came from cache", () => {
+    mockFetchedAfterMount = false;
+    mockData = {
+      dag_runs: [buildRun({ conf: { message: "From cache" }, dag_run_id: 
"run_cached" })],
+    };
+
+    render(<RecentConfigSelect dagId="test_dag" onSelectConf={vi.fn()} open 
/>, { wrapper: Wrapper });
+
+    expect(getItems()).toHaveLength(1);
+  });
+
   it("renders all distinct confs without capping the list", () => {
     mockData = {
       dag_runs: Array.from({ length: 8 }, (_unused, index) =>
diff --git 
a/airflow-core/src/airflow/ui/src/components/TriggerDag/RecentConfigSelect.tsx 
b/airflow-core/src/airflow/ui/src/components/TriggerDag/RecentConfigSelect.tsx
index 8de65ed9a48..d689da850da 100644
--- 
a/airflow-core/src/airflow/ui/src/components/TriggerDag/RecentConfigSelect.tsx
+++ 
b/airflow-core/src/airflow/ui/src/components/TriggerDag/RecentConfigSelect.tsx
@@ -18,7 +18,14 @@
  */
 import { useMemo, useState } from "react";
 
-import { createListCollection, Flex, type SelectValueChangeDetails, Text } 
from "@chakra-ui/react";
+import {
+  createListCollection,
+  Field,
+  Flex,
+  type SelectValueChangeDetails,
+  Stack,
+  Text,
+} from "@chakra-ui/react";
 import { useTranslation } from "react-i18next";
 
 import { useDagRunServiceGetDagRuns } from "openapi/queries";
@@ -42,11 +49,9 @@ type RecentConfigSelectProps = {
 const RecentConfigSelect = ({ dagId, onSelectConf, open }: 
RecentConfigSelectProps) => {
   const { t: translate } = useTranslation("components");
   const [selectedValue, setSelectedValue] = useState<Array<string>>([]);
-  const { data, isLoading } = useDagRunServiceGetDagRuns(
-    { dagId, limit: 25, orderBy: ["-run_after"] },
-    undefined,
-    { enabled: open },
-  );
+  const { data } = useDagRunServiceGetDagRuns({ dagId, limit: 25, orderBy: 
["-run_after"] }, undefined, {
+    enabled: open,
+  });
 
   const options = useMemo(() => {
     const seenConfs = new Set<string>();
@@ -79,34 +84,48 @@ const RecentConfigSelect = ({ dagId, onSelectConf, open }: 
RecentConfigSelectPro
     }
   };
 
-  if (!isLoading && options.length === 0) {
+  // The options come from the runs alone, so there is nothing to show both 
before they arrive
+  // and when none of them carried a config. Asking the query how far along it 
is would be worse
+  // than useless here: the runs stay fresh for five minutes, so reopening the 
modal serves them
+  // from cache without fetching, and a readiness flag would read that as "not 
ready yet" and
+  // hide configs that are already in hand.
+  if (options.length === 0) {
     return undefined;
   }
 
+  // Laid out as a form row like Logical Date and the run parameters below it, 
rather than a
+  // label stacked over a full-width control.
   return (
-    <Select.Root
-      collection={recentConfigOptions}
-      data-testid="recent-config-select"
-      disabled={isLoading || options.length === 0}
-      onValueChange={handleValueChange}
-      size="sm"
-      value={selectedValue}
-    >
-      <Select.Label 
fontSize="xs">{translate("triggerDag.recentConfig")}</Select.Label>
-      <Select.Trigger>
-        <Select.ValueText 
placeholder={translate("triggerDag.recentConfigPlaceholder")} />
-      </Select.Trigger>
-      <Select.Content maxH="200px" overflowY="auto">
-        {recentConfigOptions.items.map((option) => (
-          <Select.Item item={option} key={option.run.dag_run_id}>
-            <Flex justifyContent="space-between" width="100%">
-              <Text>{option.run.dag_run_id}</Text>
-              <Time datetime={option.run.run_after} />
-            </Flex>
-          </Select.Item>
-        ))}
-      </Select.Content>
-    </Select.Root>
+    <Field.Root orientation="horizontal">
+      <Stack>
+        <Field.Label fontSize="md" style={{ flexBasis: "30%" }}>
+          {translate("triggerDag.recentConfig")}
+        </Field.Label>
+      </Stack>
+      <Stack css={{ flexBasis: "70%" }}>
+        <Select.Root
+          collection={recentConfigOptions}
+          data-testid="recent-config-select"
+          onValueChange={handleValueChange}
+          size="sm"
+          value={selectedValue}
+        >
+          <Select.Trigger>
+            <Select.ValueText 
placeholder={translate("triggerDag.recentConfigPlaceholder")} />
+          </Select.Trigger>
+          <Select.Content maxH="200px" overflowY="auto">
+            {recentConfigOptions.items.map((option) => (
+              <Select.Item item={option} key={option.run.dag_run_id}>
+                <Flex justifyContent="space-between" width="100%">
+                  <Text>{option.run.dag_run_id}</Text>
+                  <Time datetime={option.run.run_after} />
+                </Flex>
+              </Select.Item>
+            ))}
+          </Select.Content>
+        </Select.Root>
+      </Stack>
+    </Field.Root>
   );
 };
 
diff --git 
a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.test.tsx 
b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.test.tsx
index d4a4e96bdb4..44040b248f8 100644
--- 
a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.test.tsx
+++ 
b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.test.tsx
@@ -79,7 +79,9 @@ vi.mock("openapi/queries", async (importOriginal) => {
           },
         ],
       },
-      isLoading: false,
+      isFetchedAfterMount: true,
+      isFetching: false,
+      isSuccess: true,
     })),
   };
 });

Reply via email to