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 480d6fa8132 Sort Dags by latest run after (#69995)
480d6fa8132 is described below

commit 480d6fa81321c234e73cc34e6323b4fbb86f33b0
Author: Shae Alhusayni <[email protected]>
AuthorDate: Tue Jul 21 06:48:57 2026 -0700

    Sort Dags by latest run after (#69995)
    
    * Sort Dags by latest run after
    
    * Remove unused Dags query type
---
 .../src/airflow/api_fastapi/common/db/dags.py      | 11 ++++--
 .../api_fastapi/core_api/openapi/_private_ui.yaml  |  4 +--
 .../airflow/api_fastapi/core_api/routes/ui/dags.py |  6 +++-
 .../ui/openapi-gen/queries/ensureQueryData.ts      |  2 +-
 .../src/airflow/ui/openapi-gen/queries/prefetch.ts |  2 +-
 .../src/airflow/ui/openapi-gen/queries/queries.ts  |  2 +-
 .../src/airflow/ui/openapi-gen/queries/suspense.ts |  2 +-
 .../ui/openapi-gen/requests/services.gen.ts        |  2 +-
 .../airflow/ui/openapi-gen/requests/types.gen.ts   |  2 +-
 .../airflow/ui/public/i18n/locales/en/dags.json    |  6 ++--
 .../src/airflow/ui/src/constants/sortParams.ts     |  8 ++---
 .../src/airflow/ui/src/mocks/handlers/dags.ts      |  9 ++++-
 .../ui/src/pages/DagsList/DagsList.test.tsx        | 39 +++++++++++++++++++-
 .../src/airflow/ui/src/pages/DagsList/DagsList.tsx |  2 +-
 .../src/airflow/ui/src/queries/useDags.tsx         |  6 +---
 .../api_fastapi/core_api/routes/ui/test_dags.py    | 41 ++++++++++++++++++++++
 16 files changed, 117 insertions(+), 27 deletions(-)

diff --git a/airflow-core/src/airflow/api_fastapi/common/db/dags.py 
b/airflow-core/src/airflow/api_fastapi/common/db/dags.py
index 41c219d8208..09cb9afc8cc 100644
--- a/airflow-core/src/airflow/api_fastapi/common/db/dags.py
+++ b/airflow-core/src/airflow/api_fastapi/common/db/dags.py
@@ -70,9 +70,14 @@ def generate_dag_with_latest_run_query(
             break
 
     requested_order_by_set = set(order_by.value) if order_by.value is not None 
else set()
-    dag_run_order_by_set = set(
-        ["last_run_state", "last_run_start_date", "-last_run_state", 
"-last_run_start_date"],
-    )
+    dag_run_order_by_set = {
+        "last_run_state",
+        "-last_run_state",
+        "last_run_start_date",
+        "-last_run_start_date",
+        "last_run_run_after",
+        "-last_run_run_after",
+    }
 
     if has_max_run_filter or (requested_order_by_set & dag_run_order_by_set):
         query = query.join(
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml 
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
index 00eaa55a5aa..a0b1b1b9821 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
+++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
@@ -510,13 +510,13 @@ paths:
             type: string
           description: 'Attributes to order by, multi criteria sort is 
supported.
             Prefix with `-` for descending order. Supported attributes: 
`dag_id, dag_display_name,
-            next_dagrun, state, start_date, last_run_state, 
last_run_start_date`'
+            next_dagrun, state, start_date, last_run_state, 
last_run_start_date, last_run_run_after`'
           default:
           - dag_id
           title: Order By
         description: 'Attributes to order by, multi criteria sort is 
supported. Prefix
           with `-` for descending order. Supported attributes: `dag_id, 
dag_display_name,
-          next_dagrun, state, start_date, last_run_state, last_run_start_date`'
+          next_dagrun, state, start_date, last_run_state, last_run_start_date, 
last_run_run_after`'
       - name: is_favorite
         in: query
         required: false
diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dags.py 
b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dags.py
index 38d0c93cef8..8e2d185d1c5 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dags.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dags.py
@@ -117,7 +117,11 @@ def get_dags(
             SortParam(
                 ["dag_id", "dag_display_name", "next_dagrun", "state", 
"start_date"],
                 DagModel,
-                {"last_run_state": DagRun.state, "last_run_start_date": 
DagRun.start_date},
+                {
+                    "last_run_state": DagRun.state,
+                    "last_run_start_date": DagRun.start_date,
+                    "last_run_run_after": DagRun.run_after,
+                },
             ).dynamic_depends()
         ),
     ],
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts 
b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
index 5984e9b22f6..15dceaa74b9 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
@@ -691,7 +691,7 @@ export const ensureUseDagServiceGetDagTagsData = 
(queryClient: QueryClient, { li
 * @param data.dagRunState Filter Dags that have any DagRun in the given state. 
Only ``queued`` and ``running`` are supported.
 * @param data.bundleName
 * @param data.bundleVersion
-* @param data.orderBy Attributes to order by, multi criteria sort is 
supported. Prefix with `-` for descending order. Supported attributes: `dag_id, 
dag_display_name, next_dagrun, state, start_date, last_run_state, 
last_run_start_date`
+* @param data.orderBy Attributes to order by, multi criteria sort is 
supported. Prefix with `-` for descending order. Supported attributes: `dag_id, 
dag_display_name, next_dagrun, state, start_date, last_run_state, 
last_run_start_date, last_run_run_after`
 * @param data.isFavorite
 * @param data.hasAssetSchedule Filter Dags with asset-based scheduling
 * @param data.assetDependency Filter Dags by asset dependency (name or URI)
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts 
b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
index cca9470844f..822dde5ad39 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
@@ -691,7 +691,7 @@ export const prefetchUseDagServiceGetDagTags = 
(queryClient: QueryClient, { limi
 * @param data.dagRunState Filter Dags that have any DagRun in the given state. 
Only ``queued`` and ``running`` are supported.
 * @param data.bundleName
 * @param data.bundleVersion
-* @param data.orderBy Attributes to order by, multi criteria sort is 
supported. Prefix with `-` for descending order. Supported attributes: `dag_id, 
dag_display_name, next_dagrun, state, start_date, last_run_state, 
last_run_start_date`
+* @param data.orderBy Attributes to order by, multi criteria sort is 
supported. Prefix with `-` for descending order. Supported attributes: `dag_id, 
dag_display_name, next_dagrun, state, start_date, last_run_state, 
last_run_start_date, last_run_run_after`
 * @param data.isFavorite
 * @param data.hasAssetSchedule Filter Dags with asset-based scheduling
 * @param data.assetDependency Filter Dags by asset dependency (name or URI)
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts 
b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
index cc0d92736ca..74cbc95ff57 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
@@ -691,7 +691,7 @@ export const useDagServiceGetDagTags = <TData = 
Common.DagServiceGetDagTagsDefau
 * @param data.dagRunState Filter Dags that have any DagRun in the given state. 
Only ``queued`` and ``running`` are supported.
 * @param data.bundleName
 * @param data.bundleVersion
-* @param data.orderBy Attributes to order by, multi criteria sort is 
supported. Prefix with `-` for descending order. Supported attributes: `dag_id, 
dag_display_name, next_dagrun, state, start_date, last_run_state, 
last_run_start_date`
+* @param data.orderBy Attributes to order by, multi criteria sort is 
supported. Prefix with `-` for descending order. Supported attributes: `dag_id, 
dag_display_name, next_dagrun, state, start_date, last_run_state, 
last_run_start_date, last_run_run_after`
 * @param data.isFavorite
 * @param data.hasAssetSchedule Filter Dags with asset-based scheduling
 * @param data.assetDependency Filter Dags by asset dependency (name or URI)
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts 
b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
index 62139d375db..962719f88aa 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
@@ -691,7 +691,7 @@ export const useDagServiceGetDagTagsSuspense = <TData = 
Common.DagServiceGetDagT
 * @param data.dagRunState Filter Dags that have any DagRun in the given state. 
Only ``queued`` and ``running`` are supported.
 * @param data.bundleName
 * @param data.bundleVersion
-* @param data.orderBy Attributes to order by, multi criteria sort is 
supported. Prefix with `-` for descending order. Supported attributes: `dag_id, 
dag_display_name, next_dagrun, state, start_date, last_run_state, 
last_run_start_date`
+* @param data.orderBy Attributes to order by, multi criteria sort is 
supported. Prefix with `-` for descending order. Supported attributes: `dag_id, 
dag_display_name, next_dagrun, state, start_date, last_run_state, 
last_run_start_date, last_run_run_after`
 * @param data.isFavorite
 * @param data.hasAssetSchedule Filter Dags with asset-based scheduling
 * @param data.assetDependency Filter Dags by asset dependency (name or URI)
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts 
b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
index ca8d6339321..dd68e8cc94b 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
@@ -1966,7 +1966,7 @@ export class DagService {
      * @param data.dagRunState Filter Dags that have any DagRun in the given 
state. Only ``queued`` and ``running`` are supported.
      * @param data.bundleName
      * @param data.bundleVersion
-     * @param data.orderBy Attributes to order by, multi criteria sort is 
supported. Prefix with `-` for descending order. Supported attributes: `dag_id, 
dag_display_name, next_dagrun, state, start_date, last_run_state, 
last_run_start_date`
+     * @param data.orderBy Attributes to order by, multi criteria sort is 
supported. Prefix with `-` for descending order. Supported attributes: `dag_id, 
dag_display_name, next_dagrun, state, start_date, last_run_state, 
last_run_start_date, last_run_run_after`
      * @param data.isFavorite
      * @param data.hasAssetSchedule Filter Dags with asset-based scheduling
      * @param data.assetDependency Filter Dags by asset dependency (name or 
URI)
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts 
b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
index 235c75ec085..7464f43b2cd 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
@@ -3559,7 +3559,7 @@ export type GetDagsUiData = {
     limit?: number;
     offset?: number;
     /**
-     * Attributes to order by, multi criteria sort is supported. Prefix with 
`-` for descending order. Supported attributes: `dag_id, dag_display_name, 
next_dagrun, state, start_date, last_run_state, last_run_start_date`
+     * Attributes to order by, multi criteria sort is supported. Prefix with 
`-` for descending order. Supported attributes: `dag_id, dag_display_name, 
next_dagrun, state, start_date, last_run_state, last_run_start_date, 
last_run_run_after`
      */
     orderBy?: Array<(string)>;
     owners?: Array<(string)>;
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/dags.json 
b/airflow-core/src/airflow/ui/public/i18n/locales/en/dags.json
index c8b195c93b8..78b8681b423 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/en/dags.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/dags.json
@@ -95,9 +95,9 @@
       "asc": "Sort by Display Name (A-Z)",
       "desc": "Sort by Display Name (Z-A)"
     },
-    "lastRunStartDate": {
-      "asc": "Sort by Latest Run Start Date (Earliest-Latest)",
-      "desc": "Sort by Latest Run Start Date (Latest-Earliest)"
+    "lastRunAfter": {
+      "asc": "Sort by Latest Run After (Earliest-Latest)",
+      "desc": "Sort by Latest Run After (Latest-Earliest)"
     },
     "lastRunState": {
       "asc": "Sort by Latest Run State (A-Z)",
diff --git a/airflow-core/src/airflow/ui/src/constants/sortParams.ts 
b/airflow-core/src/airflow/ui/src/constants/sortParams.ts
index 39864ba7f95..3d179ea9254 100644
--- a/airflow-core/src/airflow/ui/src/constants/sortParams.ts
+++ b/airflow-core/src/airflow/ui/src/constants/sortParams.ts
@@ -47,12 +47,12 @@ export const createDagSortOptions = (translate: TFunction) 
=>
         value: "-last_run_state",
       },
       {
-        label: translate("sort.lastRunStartDate.asc"),
-        value: "last_run_start_date",
+        label: translate("sort.lastRunAfter.asc"),
+        value: "last_run_run_after",
       },
       {
-        label: translate("sort.lastRunStartDate.desc"),
-        value: "-last_run_start_date",
+        label: translate("sort.lastRunAfter.desc"),
+        value: "-last_run_run_after",
       },
     ],
   });
diff --git a/airflow-core/src/airflow/ui/src/mocks/handlers/dags.ts 
b/airflow-core/src/airflow/ui/src/mocks/handlers/dags.ts
index 2c97bef0c8b..b3d4d1fea35 100644
--- a/airflow-core/src/airflow/ui/src/mocks/handlers/dags.ts
+++ b/airflow-core/src/airflow/ui/src/mocks/handlers/dags.ts
@@ -36,6 +36,7 @@ const successDag = {
       end_date: "2025-01-13T04:34:12.143831Z",
       id: 1,
       logical_date: "2025-01-13T04:33:58.396323Z",
+      run_after: "2025-01-13T04:33:58.396323Z",
       run_id: "manual__2025-01-13T04:33:58.387988+00:00",
       start_date: "2025-01-13T04:33:58.496197Z",
       state: "success",
@@ -68,6 +69,7 @@ const failedDag = {
       end_date: "2025-01-13T04:34:12.143831Z",
       id: 2,
       logical_date: "2025-01-13T04:33:58.396323Z",
+      run_after: "2025-01-13T04:33:58.396323Z",
       run_id: "manual__2025-01-13T04:33:58.387988+00:00",
       start_date: "2025-01-13T04:33:58.496197Z",
       state: "success",
@@ -121,6 +123,7 @@ export const handlers: Array<HttpHandler> = [
   http.get("/ui/dags", ({ request }) => {
     const url = new URL(request.url);
     const lastDagRunState = url.searchParams.get("last_dag_run_state");
+    const orderBy = url.searchParams.get("order_by");
     const paused = url.searchParams.get("paused");
 
     if (lastDagRunState === "success") {
@@ -135,7 +138,11 @@ export const handlers: Array<HttpHandler> = [
       });
     }
 
-    const dags = filterDagsByPaused(paused);
+    let dags = filterDagsByPaused(paused);
+
+    if (orderBy === "last_run_run_after" || orderBy === "-last_run_run_after") 
{
+      dags = [failedDag, successDag, pausedDag].filter((dag) => 
dags.includes(dag));
+    }
 
     return HttpResponse.json({
       dags,
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.test.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.test.tsx
index 271542fc5c0..7bc71a68fb0 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.test.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.test.tsx
@@ -18,10 +18,13 @@
  */
 import "@testing-library/jest-dom";
 import { render, screen, waitFor, within } from "@testing-library/react";
-import { describe, it, expect } from "vitest";
+import { afterEach, describe, expect, it } from "vitest";
 
+import { DAGS_LIST_DISPLAY_KEY } from "src/constants/localStorage";
 import { AppWrapper } from "src/utils/AppWrapper";
 
+afterEach(() => localStorage.clear());
+
 describe("Dag Filters", () => {
   it("Filter by selected last run state", async () => {
     render(<AppWrapper initialEntries={["/dags"]} />);
@@ -39,3 +42,37 @@ describe("Dag Filters", () => {
     await waitFor(() => 
expect(screen.getByText("tutorial_taskflow_api_failed")).toBeInTheDocument());
   });
 });
+
+describe("Dag sorting", () => {
+  it("sorts cards by latest run after", async () => {
+    render(<AppWrapper initialEntries={["/dags"]} />);
+
+    await waitFor(() => 
expect(screen.getByText("tutorial_taskflow_api_success")).toBeInTheDocument());
+
+    const trigger = 
within(screen.getByTestId("sort-by-select")).getByRole("combobox");
+
+    await waitFor(() => trigger.click());
+    await waitFor(() => screen.getByText("sort.lastRunAfter.desc").click());
+
+    await waitFor(() =>
+      
expect(screen.getAllByText(/tutorial_taskflow_api_/u)[0]).toHaveTextContent(
+        "tutorial_taskflow_api_failed",
+      ),
+    );
+  });
+
+  it("sorts the latest run column by run after", async () => {
+    localStorage.setItem(DAGS_LIST_DISPLAY_KEY, JSON.stringify("table"));
+    render(<AppWrapper initialEntries={["/dags"]} />);
+
+    await waitFor(() => 
expect(screen.getByTestId("table-list")).toBeInTheDocument());
+
+    screen.getByText("dagDetails.latestRun").closest("button")?.click();
+
+    await waitFor(() =>
+      
expect(screen.getAllByTestId("table-cell-dag_display_name")[0]).toHaveTextContent(
+        "tutorial_taskflow_api_failed",
+      ),
+    );
+  });
+});
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 e9c5215cc5a..56e8117a96d 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
@@ -113,7 +113,7 @@ const createColumns = (
     header: () => translate("dagDetails.nextRun"),
   },
   {
-    accessorKey: "last_run_start_date",
+    accessorKey: "last_run_run_after",
     cell: ({ row: { original } }) =>
       original.latest_dag_runs[0] ? (
         <RouterLink
diff --git a/airflow-core/src/airflow/ui/src/queries/useDags.tsx 
b/airflow-core/src/airflow/ui/src/queries/useDags.tsx
index ec4dfb18774..3517a7386e3 100644
--- a/airflow-core/src/airflow/ui/src/queries/useDags.tsx
+++ b/airflow-core/src/airflow/ui/src/queries/useDags.tsx
@@ -17,13 +17,9 @@
  * under the License.
  */
 import { useDagServiceGetDagsUi } from "openapi/queries";
-import type { DagRunState, DAGWithLatestDagRunsResponse } from 
"openapi/requests/types.gen";
+import type { DagRunState } from "openapi/requests/types.gen";
 import { isStatePending, useAutoRefresh } from "src/utils";
 
-export type DagWithLatest = {
-  last_run_start_date: string;
-} & DAGWithLatestDagRunsResponse;
-
 export const useDags = ({
   advancedSearch = false,
   dagDisplayNamePattern,
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dags.py 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dags.py
index 4e561fd3436..1cd4a70de7d 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dags.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dags.py
@@ -289,6 +289,47 @@ class TestGetDagRuns(TestPublicDagEndpoint):
             response = test_client.get("/dags")
         assert response.status_code == 403
 
+    @pytest.mark.usefixtures("configure_git_connection_for_dag_bundle")
+    def test_orders_latest_runs_by_run_after(self, test_client, session):
+        running_run_after = pendulum.datetime(2026, 1, 1, tz="UTC")
+        queued_run_after = pendulum.datetime(2026, 1, 2, tz="UTC")
+        session.add_all(
+            [
+                DagRun(
+                    dag_id=DAG1_ID,
+                    run_id="manual__running_latest",
+                    run_type=DagRunType.MANUAL,
+                    logical_date=running_run_after,
+                    run_after=running_run_after,
+                    start_date=pendulum.datetime(2026, 1, 3, tz="UTC"),
+                    state=DagRunState.RUNNING,
+                    triggered_by=DagRunTriggeredByType.TEST,
+                ),
+                DagRun(
+                    dag_id=DAG2_ID,
+                    run_id="manual__queued_latest",
+                    run_type=DagRunType.MANUAL,
+                    logical_date=queued_run_after,
+                    run_after=queued_run_after,
+                    start_date=None,
+                    state=DagRunState.QUEUED,
+                    triggered_by=DagRunTriggeredByType.TEST,
+                ),
+            ]
+        )
+        session.commit()
+
+        response = test_client.get(
+            "/dags",
+            params={
+                "dag_ids": [DAG1_ID, DAG2_ID],
+                "order_by": "-last_run_run_after",
+            },
+        )
+
+        assert response.status_code == 200
+        assert [dag["dag_id"] for dag in response.json()["dags"]] == [DAG2_ID, 
DAG1_ID]
+
     def test_get_dags_no_n_plus_one_queries(self, session, test_client):
         """Test that fetching DAGs with tags doesn't trigger n+1 queries."""
         num_dags = 5

Reply via email to