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