From bc88832c53b7bb73391af0bbfb898e47acacffc9 Mon Sep 17 00:00:00 2001 From: Dheeraj Turaga Date: Mon, 27 Jul 2026 13:08:22 -0500 Subject: [PATCH] [v3-3-test] UI: Add Run ID filter to the Grid view (#70150) The Dag Runs list page lets users narrow runs by Run ID, but the Grid view's filter bar offered no equivalent, forcing users to eyeball run columns to locate a specific run. Expose the same Run ID search on the Grid so both surfaces filter consistently. The pill exposes the same substring/prefix advanced-search toggle used elsewhere; the Grid query hooks route the value through ``useAdvancedSearchArg`` so the toggle chooses between the substring ``run_id_pattern`` and the index-friendly ``run_id_prefix_pattern`` variant on each request. (cherry picked from commit 51972109907ca274bbfa52aff3e2c71c7b02bcab) Co-authored-by: Dheeraj Turaga --- .../core_api/openapi/_private_ui.yaml | 52 +++++++++++++++++++ .../api_fastapi/core_api/routes/ui/grid.py | 34 +++++++++++- .../airflow/ui/openapi-gen/queries/common.ts | 12 +++-- .../ui/openapi-gen/queries/ensureQueryData.ts | 16 ++++-- .../ui/openapi-gen/queries/prefetch.ts | 16 ++++-- .../airflow/ui/openapi-gen/queries/queries.ts | 16 ++++-- .../ui/openapi-gen/queries/suspense.ts | 16 ++++-- .../ui/openapi-gen/requests/services.gen.ts | 12 ++++- .../ui/openapi-gen/requests/types.gen.ts | 16 ++++++ .../ui/src/layouts/Details/DetailsLayout.tsx | 5 ++ .../ui/src/layouts/Details/Gantt/Gantt.tsx | 4 ++ .../ui/src/layouts/Details/Grid/Grid.tsx | 4 ++ .../ui/src/layouts/Details/GridFilters.tsx | 1 + .../src/airflow/ui/src/queries/useGridRuns.ts | 14 +++++ .../ui/src/queries/useGridStructure.ts | 14 +++++ .../core_api/routes/ui/test_grid.py | 33 ++++++++++++ 16 files changed, 241 insertions(+), 24 deletions(-) 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 ab9b221aba66a..6e089005d8143 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 @@ -1223,6 +1223,32 @@ paths: title: Triggering User Prefix description: Case-sensitive, index-friendly prefix match. See "Filtering with pattern parameters". + - name: run_id_pattern + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + description: "Case-insensitive substring match (SQL `ILIKE`). Slower than\ + \ `run_id_prefix_pattern` on large tables \u2014 see \"Filtering with\ + \ pattern parameters\"." + title: Run Id Pattern + description: "Case-insensitive substring match (SQL `ILIKE`). Slower than\ + \ `run_id_prefix_pattern` on large tables \u2014 see \"Filtering with pattern\ + \ parameters\"." + - name: run_id_prefix_pattern + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + description: Case-sensitive, index-friendly prefix match. See "Filtering + with pattern parameters". + title: Run Id Prefix Pattern + description: Case-sensitive, index-friendly prefix match. See "Filtering with + pattern parameters". responses: '200': description: Successful Response @@ -1378,6 +1404,32 @@ paths: title: Triggering User Prefix description: Case-sensitive, index-friendly prefix match. See "Filtering with pattern parameters". + - name: run_id_pattern + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + description: "Case-insensitive substring match (SQL `ILIKE`). Slower than\ + \ `run_id_prefix_pattern` on large tables \u2014 see \"Filtering with\ + \ pattern parameters\"." + title: Run Id Pattern + description: "Case-insensitive substring match (SQL `ILIKE`). Slower than\ + \ `run_id_prefix_pattern` on large tables \u2014 see \"Filtering with pattern\ + \ parameters\"." + - name: run_id_prefix_pattern + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + description: Case-sensitive, index-friendly prefix match. See "Filtering + with pattern parameters". + title: Run Id Prefix Pattern + description: Case-sensitive, index-friendly prefix match. See "Filtering with + pattern parameters". responses: '200': description: Successful Response diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/grid.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/grid.py index ee81e2887c7f3..121cc79ec6405 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/grid.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/grid.py @@ -42,7 +42,11 @@ QueryOffset, RangeFilter, SortParam, + _PrefixSearchParam, + _SearchParam, datetime_range_filter_factory, + prefix_search_param_factory, + search_param_factory, ) from airflow.api_fastapi.common.router import AirflowRouter from airflow.api_fastapi.core_api.datamodels.ui.common import ( @@ -144,6 +148,11 @@ def get_dag_structure( state: QueryDagRunStateFilter, triggering_user: QueryDagRunTriggeringUserSearch, triggering_user_prefix: QueryDagRunTriggeringUserPrefixSearch, + run_id_pattern: Annotated[_SearchParam, Depends(search_param_factory(DagRun.run_id, "run_id_pattern"))], + run_id_prefix_pattern: Annotated[ + _PrefixSearchParam, + Depends(prefix_search_param_factory(DagRun.run_id, "run_id_prefix_pattern")), + ], include_upstream: QueryIncludeUpstream = False, include_downstream: QueryIncludeDownstream = False, depth: int | None = None, @@ -177,7 +186,15 @@ def get_dag_structure( statement=base_query, order_by=order_by, offset=offset, - filters=[run_after, run_type, state, triggering_user, triggering_user_prefix], + filters=[ + run_after, + run_type, + state, + triggering_user, + triggering_user_prefix, + run_id_pattern, + run_id_prefix_pattern, + ], limit=limit, ) run_ids = list(session.scalars(dag_runs_select_filter)) @@ -282,6 +299,11 @@ def get_grid_runs( state: QueryDagRunStateFilter, triggering_user: QueryDagRunTriggeringUserSearch, triggering_user_prefix: QueryDagRunTriggeringUserPrefixSearch, + run_id_pattern: Annotated[_SearchParam, Depends(search_param_factory(DagRun.run_id, "run_id_pattern"))], + run_id_prefix_pattern: Annotated[ + _PrefixSearchParam, + Depends(prefix_search_param_factory(DagRun.run_id, "run_id_prefix_pattern")), + ], ) -> list[GridRunsResponse]: """Get info about a run for the grid.""" # Retrieve, sort the previous Dag Runs @@ -324,7 +346,15 @@ def get_grid_runs( statement=base_query, order_by=order_by, offset=offset, - filters=[run_after, run_type, state, triggering_user, triggering_user_prefix], + filters=[ + run_after, + run_type, + state, + triggering_user, + triggering_user_prefix, + run_id_pattern, + run_id_prefix_pattern, + ], limit=limit, return_total_entries=False, ) diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts index d7baf71cf539a..eb45a01065f5a 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts @@ -975,7 +975,7 @@ export const UseStructureServiceStructureDataKeyFn = ({ dagId, depth, externalDe export type GridServiceGetDagStructureDefaultResponse = Awaited>; export type GridServiceGetDagStructureQueryResult = UseQueryResult; export const useGridServiceGetDagStructureKey = "GridServiceGetDagStructure"; -export const UseGridServiceGetDagStructureKeyFn = ({ dagId, depth, includeDownstream, includeUpstream, limit, offset, orderBy, root, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, state, triggeringUser, triggeringUserPrefix }: { +export const UseGridServiceGetDagStructureKeyFn = ({ dagId, depth, includeDownstream, includeUpstream, limit, offset, orderBy, root, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runIdPrefixPattern, runType, state, triggeringUser, triggeringUserPrefix }: { dagId: string; depth?: number; includeDownstream?: boolean; @@ -988,15 +988,17 @@ export const UseGridServiceGetDagStructureKeyFn = ({ dagId, depth, includeDownst runAfterGte?: string; runAfterLt?: string; runAfterLte?: string; + runIdPattern?: string; + runIdPrefixPattern?: string; runType?: string[]; state?: string[]; triggeringUser?: string; triggeringUserPrefix?: string; -}, queryKey?: Array) => [useGridServiceGetDagStructureKey, ...(queryKey ?? [{ dagId, depth, includeDownstream, includeUpstream, limit, offset, orderBy, root, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, state, triggeringUser, triggeringUserPrefix }])]; +}, queryKey?: Array) => [useGridServiceGetDagStructureKey, ...(queryKey ?? [{ dagId, depth, includeDownstream, includeUpstream, limit, offset, orderBy, root, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runIdPrefixPattern, runType, state, triggeringUser, triggeringUserPrefix }])]; export type GridServiceGetGridRunsDefaultResponse = Awaited>; export type GridServiceGetGridRunsQueryResult = UseQueryResult; export const useGridServiceGetGridRunsKey = "GridServiceGetGridRuns"; -export const UseGridServiceGetGridRunsKeyFn = ({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, state, triggeringUser, triggeringUserPrefix }: { +export const UseGridServiceGetGridRunsKeyFn = ({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runIdPrefixPattern, runType, state, triggeringUser, triggeringUserPrefix }: { dagId: string; limit?: number; offset?: number; @@ -1005,11 +1007,13 @@ export const UseGridServiceGetGridRunsKeyFn = ({ dagId, limit, offset, orderBy, runAfterGte?: string; runAfterLt?: string; runAfterLte?: string; + runIdPattern?: string; + runIdPrefixPattern?: string; runType?: string[]; state?: string[]; triggeringUser?: string; triggeringUserPrefix?: string; -}, queryKey?: Array) => [useGridServiceGetGridRunsKey, ...(queryKey ?? [{ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, state, triggeringUser, triggeringUserPrefix }])]; +}, queryKey?: Array) => [useGridServiceGetGridRunsKey, ...(queryKey ?? [{ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runIdPrefixPattern, runType, state, triggeringUser, triggeringUserPrefix }])]; export type GridServiceGetGridTiSummariesStreamDefaultResponse = Awaited>; export type GridServiceGetGridTiSummariesStreamQueryResult = UseQueryResult; export const useGridServiceGetGridTiSummariesStreamKey = "GridServiceGetGridTiSummariesStream"; 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 73a9f35b95bde..aa89e9e84ee53 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts @@ -1890,10 +1890,12 @@ export const ensureUseStructureServiceStructureDataData = (queryClient: QueryCli * @param data.state * @param data.triggeringUser Case-insensitive substring match (SQL `ILIKE`). Slower than `triggering_user` on large tables — see "Filtering with pattern parameters". * @param data.triggeringUserPrefix Case-sensitive, index-friendly prefix match. See "Filtering with pattern parameters". +* @param data.runIdPattern Case-insensitive substring match (SQL `ILIKE`). Slower than `run_id_prefix_pattern` on large tables — see "Filtering with pattern parameters". +* @param data.runIdPrefixPattern Case-sensitive, index-friendly prefix match. See "Filtering with pattern parameters". * @returns GridNodeResponse Successful Response * @throws ApiError */ -export const ensureUseGridServiceGetDagStructureData = (queryClient: QueryClient, { dagId, depth, includeDownstream, includeUpstream, limit, offset, orderBy, root, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, state, triggeringUser, triggeringUserPrefix }: { +export const ensureUseGridServiceGetDagStructureData = (queryClient: QueryClient, { dagId, depth, includeDownstream, includeUpstream, limit, offset, orderBy, root, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runIdPrefixPattern, runType, state, triggeringUser, triggeringUserPrefix }: { dagId: string; depth?: number; includeDownstream?: boolean; @@ -1906,11 +1908,13 @@ export const ensureUseGridServiceGetDagStructureData = (queryClient: QueryClient runAfterGte?: string; runAfterLt?: string; runAfterLte?: string; + runIdPattern?: string; + runIdPrefixPattern?: string; runType?: string[]; state?: string[]; triggeringUser?: string; triggeringUserPrefix?: string; -}) => queryClient.ensureQueryData({ queryKey: Common.UseGridServiceGetDagStructureKeyFn({ dagId, depth, includeDownstream, includeUpstream, limit, offset, orderBy, root, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, state, triggeringUser, triggeringUserPrefix }), queryFn: () => GridService.getDagStructure({ dagId, depth, includeDownstream, includeUpstream, limit, offset, orderBy, root, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, state, triggeringUser, triggeringUserPrefix }) }); +}) => queryClient.ensureQueryData({ queryKey: Common.UseGridServiceGetDagStructureKeyFn({ dagId, depth, includeDownstream, includeUpstream, limit, offset, orderBy, root, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runIdPrefixPattern, runType, state, triggeringUser, triggeringUserPrefix }), queryFn: () => GridService.getDagStructure({ dagId, depth, includeDownstream, includeUpstream, limit, offset, orderBy, root, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runIdPrefixPattern, runType, state, triggeringUser, triggeringUserPrefix }) }); /** * Get Grid Runs * Get info about a run for the grid. @@ -1927,10 +1931,12 @@ export const ensureUseGridServiceGetDagStructureData = (queryClient: QueryClient * @param data.state * @param data.triggeringUser Case-insensitive substring match (SQL `ILIKE`). Slower than `triggering_user` on large tables — see "Filtering with pattern parameters". * @param data.triggeringUserPrefix Case-sensitive, index-friendly prefix match. See "Filtering with pattern parameters". +* @param data.runIdPattern Case-insensitive substring match (SQL `ILIKE`). Slower than `run_id_prefix_pattern` on large tables — see "Filtering with pattern parameters". +* @param data.runIdPrefixPattern Case-sensitive, index-friendly prefix match. See "Filtering with pattern parameters". * @returns GridRunsResponse Successful Response * @throws ApiError */ -export const ensureUseGridServiceGetGridRunsData = (queryClient: QueryClient, { dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, state, triggeringUser, triggeringUserPrefix }: { +export const ensureUseGridServiceGetGridRunsData = (queryClient: QueryClient, { dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runIdPrefixPattern, runType, state, triggeringUser, triggeringUserPrefix }: { dagId: string; limit?: number; offset?: number; @@ -1939,11 +1945,13 @@ export const ensureUseGridServiceGetGridRunsData = (queryClient: QueryClient, { runAfterGte?: string; runAfterLt?: string; runAfterLte?: string; + runIdPattern?: string; + runIdPrefixPattern?: string; runType?: string[]; state?: string[]; triggeringUser?: string; triggeringUserPrefix?: string; -}) => queryClient.ensureQueryData({ queryKey: Common.UseGridServiceGetGridRunsKeyFn({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, state, triggeringUser, triggeringUserPrefix }), queryFn: () => GridService.getGridRuns({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, state, triggeringUser, triggeringUserPrefix }) }); +}) => queryClient.ensureQueryData({ queryKey: Common.UseGridServiceGetGridRunsKeyFn({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runIdPrefixPattern, runType, state, triggeringUser, triggeringUserPrefix }), queryFn: () => GridService.getGridRuns({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runIdPrefixPattern, runType, state, triggeringUser, triggeringUserPrefix }) }); /** * Get Grid Ti Summaries Stream * Stream TI summaries for multiple Dag runs as NDJSON (one JSON line per run). 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 bc41b8baad5ac..7f3668ce6e432 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts @@ -1890,10 +1890,12 @@ export const prefetchUseStructureServiceStructureData = (queryClient: QueryClien * @param data.state * @param data.triggeringUser Case-insensitive substring match (SQL `ILIKE`). Slower than `triggering_user` on large tables — see "Filtering with pattern parameters". * @param data.triggeringUserPrefix Case-sensitive, index-friendly prefix match. See "Filtering with pattern parameters". +* @param data.runIdPattern Case-insensitive substring match (SQL `ILIKE`). Slower than `run_id_prefix_pattern` on large tables — see "Filtering with pattern parameters". +* @param data.runIdPrefixPattern Case-sensitive, index-friendly prefix match. See "Filtering with pattern parameters". * @returns GridNodeResponse Successful Response * @throws ApiError */ -export const prefetchUseGridServiceGetDagStructure = (queryClient: QueryClient, { dagId, depth, includeDownstream, includeUpstream, limit, offset, orderBy, root, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, state, triggeringUser, triggeringUserPrefix }: { +export const prefetchUseGridServiceGetDagStructure = (queryClient: QueryClient, { dagId, depth, includeDownstream, includeUpstream, limit, offset, orderBy, root, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runIdPrefixPattern, runType, state, triggeringUser, triggeringUserPrefix }: { dagId: string; depth?: number; includeDownstream?: boolean; @@ -1906,11 +1908,13 @@ export const prefetchUseGridServiceGetDagStructure = (queryClient: QueryClient, runAfterGte?: string; runAfterLt?: string; runAfterLte?: string; + runIdPattern?: string; + runIdPrefixPattern?: string; runType?: string[]; state?: string[]; triggeringUser?: string; triggeringUserPrefix?: string; -}) => queryClient.prefetchQuery({ queryKey: Common.UseGridServiceGetDagStructureKeyFn({ dagId, depth, includeDownstream, includeUpstream, limit, offset, orderBy, root, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, state, triggeringUser, triggeringUserPrefix }), queryFn: () => GridService.getDagStructure({ dagId, depth, includeDownstream, includeUpstream, limit, offset, orderBy, root, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, state, triggeringUser, triggeringUserPrefix }) }); +}) => queryClient.prefetchQuery({ queryKey: Common.UseGridServiceGetDagStructureKeyFn({ dagId, depth, includeDownstream, includeUpstream, limit, offset, orderBy, root, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runIdPrefixPattern, runType, state, triggeringUser, triggeringUserPrefix }), queryFn: () => GridService.getDagStructure({ dagId, depth, includeDownstream, includeUpstream, limit, offset, orderBy, root, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runIdPrefixPattern, runType, state, triggeringUser, triggeringUserPrefix }) }); /** * Get Grid Runs * Get info about a run for the grid. @@ -1927,10 +1931,12 @@ export const prefetchUseGridServiceGetDagStructure = (queryClient: QueryClient, * @param data.state * @param data.triggeringUser Case-insensitive substring match (SQL `ILIKE`). Slower than `triggering_user` on large tables — see "Filtering with pattern parameters". * @param data.triggeringUserPrefix Case-sensitive, index-friendly prefix match. See "Filtering with pattern parameters". +* @param data.runIdPattern Case-insensitive substring match (SQL `ILIKE`). Slower than `run_id_prefix_pattern` on large tables — see "Filtering with pattern parameters". +* @param data.runIdPrefixPattern Case-sensitive, index-friendly prefix match. See "Filtering with pattern parameters". * @returns GridRunsResponse Successful Response * @throws ApiError */ -export const prefetchUseGridServiceGetGridRuns = (queryClient: QueryClient, { dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, state, triggeringUser, triggeringUserPrefix }: { +export const prefetchUseGridServiceGetGridRuns = (queryClient: QueryClient, { dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runIdPrefixPattern, runType, state, triggeringUser, triggeringUserPrefix }: { dagId: string; limit?: number; offset?: number; @@ -1939,11 +1945,13 @@ export const prefetchUseGridServiceGetGridRuns = (queryClient: QueryClient, { da runAfterGte?: string; runAfterLt?: string; runAfterLte?: string; + runIdPattern?: string; + runIdPrefixPattern?: string; runType?: string[]; state?: string[]; triggeringUser?: string; triggeringUserPrefix?: string; -}) => queryClient.prefetchQuery({ queryKey: Common.UseGridServiceGetGridRunsKeyFn({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, state, triggeringUser, triggeringUserPrefix }), queryFn: () => GridService.getGridRuns({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, state, triggeringUser, triggeringUserPrefix }) }); +}) => queryClient.prefetchQuery({ queryKey: Common.UseGridServiceGetGridRunsKeyFn({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runIdPrefixPattern, runType, state, triggeringUser, triggeringUserPrefix }), queryFn: () => GridService.getGridRuns({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runIdPrefixPattern, runType, state, triggeringUser, triggeringUserPrefix }) }); /** * Get Grid Ti Summaries Stream * Stream TI summaries for multiple Dag runs as NDJSON (one JSON line per run). 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 6ac7a2fb5c2f1..0f705389911dd 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts @@ -1890,10 +1890,12 @@ export const useStructureServiceStructureData = = unknown[]>({ dagId, depth, includeDownstream, includeUpstream, limit, offset, orderBy, root, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, state, triggeringUser, triggeringUserPrefix }: { +export const useGridServiceGetDagStructure = = unknown[]>({ dagId, depth, includeDownstream, includeUpstream, limit, offset, orderBy, root, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runIdPrefixPattern, runType, state, triggeringUser, triggeringUserPrefix }: { dagId: string; depth?: number; includeDownstream?: boolean; @@ -1906,11 +1908,13 @@ export const useGridServiceGetDagStructure = , "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseGridServiceGetDagStructureKeyFn({ dagId, depth, includeDownstream, includeUpstream, limit, offset, orderBy, root, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, state, triggeringUser, triggeringUserPrefix }, queryKey), queryFn: () => GridService.getDagStructure({ dagId, depth, includeDownstream, includeUpstream, limit, offset, orderBy, root, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, state, triggeringUser, triggeringUserPrefix }) as TData, ...options }); +}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseGridServiceGetDagStructureKeyFn({ dagId, depth, includeDownstream, includeUpstream, limit, offset, orderBy, root, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runIdPrefixPattern, runType, state, triggeringUser, triggeringUserPrefix }, queryKey), queryFn: () => GridService.getDagStructure({ dagId, depth, includeDownstream, includeUpstream, limit, offset, orderBy, root, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runIdPrefixPattern, runType, state, triggeringUser, triggeringUserPrefix }) as TData, ...options }); /** * Get Grid Runs * Get info about a run for the grid. @@ -1927,10 +1931,12 @@ export const useGridServiceGetDagStructure = = unknown[]>({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, state, triggeringUser, triggeringUserPrefix }: { +export const useGridServiceGetGridRuns = = unknown[]>({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runIdPrefixPattern, runType, state, triggeringUser, triggeringUserPrefix }: { dagId: string; limit?: number; offset?: number; @@ -1939,11 +1945,13 @@ export const useGridServiceGetGridRuns = , "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseGridServiceGetGridRunsKeyFn({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, state, triggeringUser, triggeringUserPrefix }, queryKey), queryFn: () => GridService.getGridRuns({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, state, triggeringUser, triggeringUserPrefix }) as TData, ...options }); +}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseGridServiceGetGridRunsKeyFn({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runIdPrefixPattern, runType, state, triggeringUser, triggeringUserPrefix }, queryKey), queryFn: () => GridService.getGridRuns({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runIdPrefixPattern, runType, state, triggeringUser, triggeringUserPrefix }) as TData, ...options }); /** * Get Grid Ti Summaries Stream * Stream TI summaries for multiple Dag runs as NDJSON (one JSON line per run). 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 54edc11c8b05e..49eeace68806a 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts @@ -1890,10 +1890,12 @@ export const useStructureServiceStructureDataSuspense = = unknown[]>({ dagId, depth, includeDownstream, includeUpstream, limit, offset, orderBy, root, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, state, triggeringUser, triggeringUserPrefix }: { +export const useGridServiceGetDagStructureSuspense = = unknown[]>({ dagId, depth, includeDownstream, includeUpstream, limit, offset, orderBy, root, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runIdPrefixPattern, runType, state, triggeringUser, triggeringUserPrefix }: { dagId: string; depth?: number; includeDownstream?: boolean; @@ -1906,11 +1908,13 @@ export const useGridServiceGetDagStructureSuspense = , "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseGridServiceGetDagStructureKeyFn({ dagId, depth, includeDownstream, includeUpstream, limit, offset, orderBy, root, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, state, triggeringUser, triggeringUserPrefix }, queryKey), queryFn: () => GridService.getDagStructure({ dagId, depth, includeDownstream, includeUpstream, limit, offset, orderBy, root, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, state, triggeringUser, triggeringUserPrefix }) as TData, ...options }); +}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseGridServiceGetDagStructureKeyFn({ dagId, depth, includeDownstream, includeUpstream, limit, offset, orderBy, root, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runIdPrefixPattern, runType, state, triggeringUser, triggeringUserPrefix }, queryKey), queryFn: () => GridService.getDagStructure({ dagId, depth, includeDownstream, includeUpstream, limit, offset, orderBy, root, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runIdPrefixPattern, runType, state, triggeringUser, triggeringUserPrefix }) as TData, ...options }); /** * Get Grid Runs * Get info about a run for the grid. @@ -1927,10 +1931,12 @@ export const useGridServiceGetDagStructureSuspense = = unknown[]>({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, state, triggeringUser, triggeringUserPrefix }: { +export const useGridServiceGetGridRunsSuspense = = unknown[]>({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runIdPrefixPattern, runType, state, triggeringUser, triggeringUserPrefix }: { dagId: string; limit?: number; offset?: number; @@ -1939,11 +1945,13 @@ export const useGridServiceGetGridRunsSuspense = , "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseGridServiceGetGridRunsKeyFn({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, state, triggeringUser, triggeringUserPrefix }, queryKey), queryFn: () => GridService.getGridRuns({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, state, triggeringUser, triggeringUserPrefix }) as TData, ...options }); +}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseGridServiceGetGridRunsKeyFn({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runIdPrefixPattern, runType, state, triggeringUser, triggeringUserPrefix }, queryKey), queryFn: () => GridService.getGridRuns({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runIdPattern, runIdPrefixPattern, runType, state, triggeringUser, triggeringUserPrefix }) as TData, ...options }); /** * Get Grid Ti Summaries Stream * Stream TI summaries for multiple Dag runs as NDJSON (one JSON line per run). 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 bb1fa6a674730..78f6b2c121849 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 @@ -4878,6 +4878,8 @@ export class GridService { * @param data.state * @param data.triggeringUser Case-insensitive substring match (SQL `ILIKE`). Slower than `triggering_user` on large tables — see "Filtering with pattern parameters". * @param data.triggeringUserPrefix Case-sensitive, index-friendly prefix match. See "Filtering with pattern parameters". + * @param data.runIdPattern Case-insensitive substring match (SQL `ILIKE`). Slower than `run_id_prefix_pattern` on large tables — see "Filtering with pattern parameters". + * @param data.runIdPrefixPattern Case-sensitive, index-friendly prefix match. See "Filtering with pattern parameters". * @returns GridNodeResponse Successful Response * @throws ApiError */ @@ -4903,7 +4905,9 @@ export class GridService { run_type: data.runType, state: data.state, triggering_user: data.triggeringUser, - triggering_user_prefix: data.triggeringUserPrefix + triggering_user_prefix: data.triggeringUserPrefix, + run_id_pattern: data.runIdPattern, + run_id_prefix_pattern: data.runIdPrefixPattern }, errors: { 400: 'Bad Request', @@ -4929,6 +4933,8 @@ export class GridService { * @param data.state * @param data.triggeringUser Case-insensitive substring match (SQL `ILIKE`). Slower than `triggering_user` on large tables — see "Filtering with pattern parameters". * @param data.triggeringUserPrefix Case-sensitive, index-friendly prefix match. See "Filtering with pattern parameters". + * @param data.runIdPattern Case-insensitive substring match (SQL `ILIKE`). Slower than `run_id_prefix_pattern` on large tables — see "Filtering with pattern parameters". + * @param data.runIdPrefixPattern Case-sensitive, index-friendly prefix match. See "Filtering with pattern parameters". * @returns GridRunsResponse Successful Response * @throws ApiError */ @@ -4950,7 +4956,9 @@ export class GridService { run_type: data.runType, state: data.state, triggering_user: data.triggeringUser, - triggering_user_prefix: data.triggeringUserPrefix + triggering_user_prefix: data.triggeringUserPrefix, + run_id_pattern: data.runIdPattern, + run_id_prefix_pattern: data.runIdPrefixPattern }, errors: { 400: 'Bad Request', 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 dc81f64b4f929..80b6c188230df 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 @@ -4512,6 +4512,14 @@ export type GetDagStructureData = { runAfterGte?: string | null; runAfterLt?: string | null; runAfterLte?: string | null; + /** + * Case-insensitive substring match (SQL `ILIKE`). Slower than `run_id_prefix_pattern` on large tables — see "Filtering with pattern parameters". + */ + runIdPattern?: string | null; + /** + * Case-sensitive, index-friendly prefix match. See "Filtering with pattern parameters". + */ + runIdPrefixPattern?: string | null; runType?: Array<(string)>; state?: Array<(string)>; /** @@ -4538,6 +4546,14 @@ export type GetGridRunsData = { runAfterGte?: string | null; runAfterLt?: string | null; runAfterLte?: string | null; + /** + * Case-insensitive substring match (SQL `ILIKE`). Slower than `run_id_prefix_pattern` on large tables — see "Filtering with pattern parameters". + */ + runIdPattern?: string | null; + /** + * Case-sensitive, index-friendly prefix match. See "Filtering with pattern parameters". + */ + runIdPrefixPattern?: string | null; runType?: Array<(string)>; state?: Array<(string)>; /** diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx index c55f3bd0d3709..d5f0431f81a11 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx @@ -134,6 +134,7 @@ export const DetailsLayout = ({ children, error, isLoading, outletContext, tabs const runAfterLte = searchParams.get(SearchParamsKeys.RUN_AFTER_LTE) ?? undefined; const runTypeFilter = (searchParams.get(SearchParamsKeys.RUN_TYPE) as DagRunType | null) ?? undefined; const triggeringUserFilter = searchParams.get(SearchParamsKeys.TRIGGERING_USER_NAME_PATTERN) ?? undefined; + const runIdPatternFilter = searchParams.get(SearchParamsKeys.RUN_ID_PATTERN) ?? undefined; const dagRunStateFilter = (searchParams.get(SearchParamsKeys.STATE) as DagRunState | null) ?? undefined; // --- Setters that write back to URL --- @@ -164,6 +165,7 @@ export const DetailsLayout = ({ children, error, isLoading, outletContext, tabs const { data: initialGridRuns } = useGridRuns({ dagRunState: dagRunStateFilter, limit, + runIdPattern: runIdPatternFilter, runType: runTypeFilter, triggeringUser: triggeringUserFilter, }); @@ -289,6 +291,7 @@ export const DetailsLayout = ({ children, error, isLoading, outletContext, tabs onJumpToLatest={handleJumpToLatest} runAfterGte={runAfterGte} runAfterLte={runAfterLte} + runIdPattern={runIdPatternFilter} runType={runTypeFilter} setOffset={setOffset} sharedScrollContainerRef={sharedGridGanttScrollRef} @@ -302,6 +305,7 @@ export const DetailsLayout = ({ children, error, isLoading, outletContext, tabs offset={offset} runAfterGte={runAfterGte} runAfterLte={runAfterLte} + runIdPattern={runIdPatternFilter} runType={runTypeFilter} sharedScrollContainerRef={sharedGridGanttScrollRef} triggeringUser={triggeringUserFilter} @@ -325,6 +329,7 @@ export const DetailsLayout = ({ children, error, isLoading, outletContext, tabs onJumpToLatest={handleJumpToLatest} runAfterGte={runAfterGte} runAfterLte={runAfterLte} + runIdPattern={runIdPatternFilter} runType={runTypeFilter} setOffset={setOffset} showVersionIndicatorMode={showVersionIndicatorMode} diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx index 6383d57623a35..ad7b3e68cb2e8 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx @@ -48,6 +48,7 @@ type Props = { readonly offset?: number; readonly runAfterGte?: string | undefined; readonly runAfterLte?: string | undefined; + readonly runIdPattern?: string | undefined; readonly runType?: DagRunType | undefined; readonly sharedScrollContainerRef?: RefObject; readonly triggeringUser?: string | undefined; @@ -59,6 +60,7 @@ export const Gantt = ({ offset, runAfterGte, runAfterLte, + runIdPattern, runType, sharedScrollContainerRef, triggeringUser, @@ -85,6 +87,7 @@ export const Gantt = ({ offset, runAfterGte, runAfterLte, + runIdPattern, runType, triggeringUser, }); @@ -95,6 +98,7 @@ export const Gantt = ({ includeUpstream, limit, root: filterRoot, + runIdPattern, runType, triggeringUser, }); diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Grid.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Grid.tsx index a1cb9d36e991c..ff122b47b2d96 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Grid.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/Grid/Grid.tsx @@ -53,6 +53,7 @@ type Props = { readonly onJumpToLatest: () => void; readonly runAfterGte?: string; readonly runAfterLte?: string; + readonly runIdPattern?: string | undefined; readonly runType?: DagRunType | undefined; readonly setOffset: (value: number) => void; readonly sharedScrollContainerRef?: RefObject; @@ -70,6 +71,7 @@ export const Grid = ({ onJumpToLatest, runAfterGte, runAfterLte, + runIdPattern, runType, setOffset, sharedScrollContainerRef, @@ -99,6 +101,7 @@ export const Grid = ({ offset, runAfterGte, runAfterLte, + runIdPattern, runType, triggeringUser, }); @@ -121,6 +124,7 @@ export const Grid = ({ includeUpstream, limit, root: filterRoot, + runIdPattern, runType, triggeringUser, }); diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/GridFilters.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/GridFilters.tsx index fe1ea80ab1afe..f11b1098ac6d4 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/GridFilters.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/GridFilters.tsx @@ -21,6 +21,7 @@ import { SearchParamsKeys } from "src/constants/searchParams"; import { useFiltersHandler, type FilterableSearchParamsKeys } from "src/utils"; const searchParamKeys: Array = [ + SearchParamsKeys.RUN_ID_PATTERN, SearchParamsKeys.STATE, SearchParamsKeys.RUN_TYPE, SearchParamsKeys.TRIGGERING_USER_NAME_PATTERN, diff --git a/airflow-core/src/airflow/ui/src/queries/useGridRuns.ts b/airflow-core/src/airflow/ui/src/queries/useGridRuns.ts index f487de7a03f2a..fe6340f08ad86 100644 --- a/airflow-core/src/airflow/ui/src/queries/useGridRuns.ts +++ b/airflow-core/src/airflow/ui/src/queries/useGridRuns.ts @@ -20,6 +20,8 @@ import { useParams } from "react-router-dom"; import { useGridServiceGetGridRuns } from "openapi/queries"; import type { DagRunState, DagRunType } from "openapi/requests/types.gen"; +import { SearchParamsKeys } from "src/constants/searchParams"; +import { useAdvancedSearchArg } from "src/hooks/useAdvancedSearch"; import { isStatePending, useAutoRefresh } from "src/utils"; export const useGridRuns = ({ @@ -28,6 +30,7 @@ export const useGridRuns = ({ offset, runAfterGte, runAfterLte, + runIdPattern, runType, triggeringUser, }: { @@ -36,6 +39,7 @@ export const useGridRuns = ({ offset?: number; runAfterGte?: string; runAfterLte?: string; + runIdPattern?: string | undefined; runType?: DagRunType | undefined; triggeringUser?: string | undefined; }) => { @@ -43,6 +47,15 @@ export const useGridRuns = ({ const refetchInterval = useAutoRefresh({ dagId }); + // Advanced-search toggle picks between the substring ``runIdPattern`` and the + // index-friendly ``runIdPrefixPattern`` variants of the Run ID filter. + const runIdPatternArg = useAdvancedSearchArg({ + patternApiKey: "runIdPattern", + prefixApiKey: "runIdPrefixPattern", + storageKey: SearchParamsKeys.RUN_ID_PATTERN, + value: runIdPattern, + }); + const { data: GridRuns, ...rest } = useGridServiceGetGridRuns( { dagId, @@ -51,6 +64,7 @@ export const useGridRuns = ({ orderBy: ["-run_after"], runAfterGte: runAfterGte ?? undefined, runAfterLte: runAfterLte ?? undefined, + ...runIdPatternArg, runType: runType ? [runType] : undefined, state: dagRunState ? [dagRunState] : undefined, triggeringUser: triggeringUser ?? undefined, diff --git a/airflow-core/src/airflow/ui/src/queries/useGridStructure.ts b/airflow-core/src/airflow/ui/src/queries/useGridStructure.ts index bbae7d789dac3..702145b2bf55d 100644 --- a/airflow-core/src/airflow/ui/src/queries/useGridStructure.ts +++ b/airflow-core/src/airflow/ui/src/queries/useGridStructure.ts @@ -20,6 +20,8 @@ import { useParams } from "react-router-dom"; import { useGridServiceGetDagStructure } from "openapi/queries"; import type { DagRunState, DagRunType } from "openapi/requests/types.gen"; +import { SearchParamsKeys } from "src/constants/searchParams"; +import { useAdvancedSearchArg } from "src/hooks/useAdvancedSearch"; import { useAutoRefresh } from "src/utils"; export const useGridStructure = ({ @@ -30,6 +32,7 @@ export const useGridStructure = ({ includeUpstream, limit, root, + runIdPattern, runType, triggeringUser, }: { @@ -40,12 +43,22 @@ export const useGridStructure = ({ includeUpstream?: boolean; limit?: number; root?: string; + runIdPattern?: string | undefined; runType?: DagRunType | undefined; triggeringUser?: string | undefined; }) => { const { dagId = "" } = useParams(); const refetchInterval = useAutoRefresh({ dagId }); + // Advanced-search toggle picks between the substring ``runIdPattern`` and the + // index-friendly ``runIdPrefixPattern`` variants of the Run ID filter. + const runIdPatternArg = useAdvancedSearchArg({ + patternApiKey: "runIdPattern", + prefixApiKey: "runIdPrefixPattern", + storageKey: SearchParamsKeys.RUN_ID_PATTERN, + value: runIdPattern, + }); + // This is necessary for keepPreviousData const { data: dagStructure, ...rest } = useGridServiceGetDagStructure( { @@ -56,6 +69,7 @@ export const useGridStructure = ({ limit, orderBy: ["-run_after"], root, + ...runIdPatternArg, runType: runType ? [runType] : undefined, state: dagRunState ? [dagRunState] : undefined, triggeringUser: triggeringUser ?? undefined, diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_grid.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_grid.py index d55b2df36252e..2b974148c6845 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_grid.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_grid.py @@ -704,6 +704,39 @@ def test_get_grid_runs_filter_by_run_type_and_triggering_user(self, session, tes assert response.status_code == 200 assert _strip_dag_version_ids(response.json()) == [GRID_RUN_2] + @pytest.mark.parametrize( + ("endpoint", "run_id_pattern", "expected"), + [ + ("runs", "run_1", [GRID_RUN_1]), + ("runs", "run_2", [GRID_RUN_2]), + ("runs", "RUN_1", [GRID_RUN_1]), # ILIKE substring search is case-insensitive + ("runs", "nonexistent", []), + ("structure", "run_1", GRID_NODES), + ], + ) + def test_filter_by_run_id_pattern(self, session, test_client, endpoint, run_id_pattern, expected): + session.commit() + response = test_client.get(f"/grid/{endpoint}/{DAG_ID}?run_id_pattern={run_id_pattern}") + assert response.status_code == 200 + assert _strip_dag_version_ids(response.json()) == expected + + @pytest.mark.parametrize( + ("endpoint", "run_id_prefix_pattern", "expected"), + [ + ("runs", "run_1", [GRID_RUN_1]), + ("runs", "run_2", [GRID_RUN_2]), + ("runs", "nonexistent", []), + ("structure", "run_1", GRID_NODES), + ], + ) + def test_filter_by_run_id_prefix_pattern( + self, session, test_client, endpoint, run_id_prefix_pattern, expected + ): + session.commit() + response = test_client.get(f"/grid/{endpoint}/{DAG_ID}?run_id_prefix_pattern={run_id_prefix_pattern}") + assert response.status_code == 200 + assert _strip_dag_version_ids(response.json()) == expected + @pytest.mark.parametrize( ("endpoint", "state", "expected"), [