This is an automated email from the ASF dual-hosted git repository. jscheffl 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 e9ec8929810 Add run type and triggering user filters to grid view (#55082) e9ec8929810 is described below commit e9ec89298102d2c31bd77f1c2ad1e4e5f2027499 Author: Dheeraj Turaga <dheerajtur...@gmail.com> AuthorDate: Mon Sep 8 15:52:25 2025 -0500 Add run type and triggering user filters to grid view (#55082) * Add run type filter to grid view for DAG run columns Add a run type filter to the grid view that allows users to filter DAG run columns by run type (scheduled, manual, backfill, asset_triggered). The filter is available in the grid options panel and persists user selection per DAG using localStorage. * Fix translations * Add triggering user filter to grid view - Add QueryDagRunTriggeringUserSearch parameter to filter by triggering_user_name - Update grid API endpoints to accept triggering_user parameter - Add triggering user search input to grid options panel - Implement state persistence with localStorage per DAG - Update OpenAPI generated types and services to support new parameter - Modify useGridRuns and useGridStructure hooks to pass triggeringUser - Add English translations for "Triggering User Name" label - Enable real-time filtering of DAG runs by username Users can now filter grid view columns to show only runs triggered by specific users through a search field in the options panel. * Fix run type filter display showing raw translation keys - Update Select.ValueText in PanelButtons to use custom render function - Display translated labels instead of raw keys like "dags:filters.allRunTypes" - Add RunTypeIcon integration for selected run types to match DagRuns page - Properly handle "all" selection with translated "All Run Types" text - Match visual style and behavior of existing DagRuns.tsx implementation * Refactor translation keys to reuse existing common.json entries * Pierre's Suggestions to use searchbox * Add tests! * consolidating test cases with @pytest.mark.parametrize --- .../src/airflow/api_fastapi/common/parameters.py | 4 + .../api_fastapi/core_api/openapi/_private_ui.yaml | 40 ++++++++++ .../airflow/api_fastapi/core_api/routes/ui/grid.py | 10 ++- .../src/airflow/ui/openapi-gen/queries/common.ts | 12 ++- .../ui/openapi-gen/queries/ensureQueryData.ts | 16 +++- .../src/airflow/ui/openapi-gen/queries/prefetch.ts | 16 +++- .../src/airflow/ui/openapi-gen/queries/queries.ts | 16 +++- .../src/airflow/ui/openapi-gen/queries/suspense.ts | 16 +++- .../ui/openapi-gen/requests/services.gen.ts | 12 ++- .../airflow/ui/openapi-gen/requests/types.gen.ts | 10 +++ .../airflow/ui/public/i18n/locales/en/common.json | 3 +- .../ui/src/layouts/Details/DetailsLayout.tsx | 20 ++++- .../airflow/ui/src/layouts/Details/Grid/Grid.tsx | 10 ++- .../ui/src/layouts/Details/PanelButtons.tsx | 86 ++++++++++++++++++++++ .../src/airflow/ui/src/queries/useGridRuns.ts | 13 +++- .../src/airflow/ui/src/queries/useGridStructure.ts | 7 ++ .../api_fastapi/core_api/routes/ui/test_grid.py | 37 ++++++++++ 17 files changed, 297 insertions(+), 31 deletions(-) diff --git a/airflow-core/src/airflow/api_fastapi/common/parameters.py b/airflow-core/src/airflow/api_fastapi/common/parameters.py index cae6bdac0f0..f1d876f29b6 100644 --- a/airflow-core/src/airflow/api_fastapi/common/parameters.py +++ b/airflow-core/src/airflow/api_fastapi/common/parameters.py @@ -797,6 +797,10 @@ QueryDagRunRunTypesFilter = Annotated[ ), ] +QueryDagRunTriggeringUserSearch = Annotated[ + _SearchParam, Depends(search_param_factory(DagRun.triggering_user_name, "triggering_user")) +] + # DagTags QueryDagTagPatternSearch = Annotated[ _SearchParam, Depends(search_param_factory(DagTag.name, "tag_name_pattern")) 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 d4e9553459f..a4d85b16dd3 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 @@ -664,6 +664,26 @@ paths: format: date-time - type: 'null' title: Run After Lt + - name: run_type + in: query + required: false + schema: + type: array + items: + type: string + title: Run Type + - name: triggering_user + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + description: "SQL LIKE expression \u2014 use `%` / `_` wildcards (e.g. `%customer_%`).\ + \ Regular expressions are **not** supported." + title: Triggering User + description: "SQL LIKE expression \u2014 use `%` / `_` wildcards (e.g. `%customer_%`).\ + \ Regular expressions are **not** supported." responses: '200': description: Successful Response @@ -771,6 +791,26 @@ paths: format: date-time - type: 'null' title: Run After Lt + - name: run_type + in: query + required: false + schema: + type: array + items: + type: string + title: Run Type + - name: triggering_user + in: query + required: false + schema: + anyOf: + - type: string + - type: 'null' + description: "SQL LIKE expression \u2014 use `%` / `_` wildcards (e.g. `%customer_%`).\ + \ Regular expressions are **not** supported." + title: Triggering User + description: "SQL LIKE expression \u2014 use `%` / `_` wildcards (e.g. `%customer_%`).\ + \ Regular expressions are **not** supported." 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 0f9f4023cbc..7ff49ac27c1 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 @@ -27,6 +27,8 @@ from sqlalchemy import select from airflow.api_fastapi.auth.managers.models.resource_details import DagAccessEntity from airflow.api_fastapi.common.db.common import SessionDep, paginated_select from airflow.api_fastapi.common.parameters import ( + QueryDagRunRunTypesFilter, + QueryDagRunTriggeringUserSearch, QueryLimit, QueryOffset, RangeFilter, @@ -118,6 +120,8 @@ def get_dag_structure( Depends(SortParam(["run_after", "logical_date", "start_date", "end_date"], DagRun).dynamic_depends()), ], run_after: Annotated[RangeFilter, Depends(datetime_range_filter_factory("run_after", DagRun))], + run_type: QueryDagRunRunTypesFilter, + triggering_user: QueryDagRunTriggeringUserSearch, ) -> list[GridNodeResponse]: """Return dag structure for grid view.""" latest_serdag = _get_latest_serdag(dag_id, session) @@ -136,7 +140,7 @@ def get_dag_structure( statement=base_query, order_by=order_by, offset=offset, - filters=[run_after], + filters=[run_after, run_type, triggering_user], limit=limit, ) run_ids = list(session.scalars(dag_runs_select_filter)) @@ -214,6 +218,8 @@ def get_grid_runs( ), ], run_after: Annotated[RangeFilter, Depends(datetime_range_filter_factory("run_after", DagRun))], + run_type: QueryDagRunRunTypesFilter, + triggering_user: QueryDagRunTriggeringUserSearch, ) -> list[GridRunsResponse]: """Get info about a run for the grid.""" # Retrieve, sort the previous DAG Runs @@ -241,7 +247,7 @@ def get_grid_runs( statement=base_query, order_by=order_by, offset=offset, - filters=[run_after], + filters=[run_after, run_type, triggering_user], limit=limit, ) return session.execute(dag_runs_select_filter) 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 6322b959a38..457aac893ee 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts @@ -794,7 +794,7 @@ export const UseStructureServiceStructureDataKeyFn = ({ dagId, externalDependenc export type GridServiceGetDagStructureDefaultResponse = Awaited<ReturnType<typeof GridService.getDagStructure>>; export type GridServiceGetDagStructureQueryResult<TData = GridServiceGetDagStructureDefaultResponse, TError = unknown> = UseQueryResult<TData, TError>; export const useGridServiceGetDagStructureKey = "GridServiceGetDagStructure"; -export const UseGridServiceGetDagStructureKeyFn = ({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte }: { +export const UseGridServiceGetDagStructureKeyFn = ({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, triggeringUser }: { dagId: string; limit?: number; offset?: number; @@ -803,11 +803,13 @@ export const UseGridServiceGetDagStructureKeyFn = ({ dagId, limit, offset, order runAfterGte?: string; runAfterLt?: string; runAfterLte?: string; -}, queryKey?: Array<unknown>) => [useGridServiceGetDagStructureKey, ...(queryKey ?? [{ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte }])]; + runType?: string[]; + triggeringUser?: string; +}, queryKey?: Array<unknown>) => [useGridServiceGetDagStructureKey, ...(queryKey ?? [{ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, triggeringUser }])]; export type GridServiceGetGridRunsDefaultResponse = Awaited<ReturnType<typeof GridService.getGridRuns>>; export type GridServiceGetGridRunsQueryResult<TData = GridServiceGetGridRunsDefaultResponse, TError = unknown> = UseQueryResult<TData, TError>; export const useGridServiceGetGridRunsKey = "GridServiceGetGridRuns"; -export const UseGridServiceGetGridRunsKeyFn = ({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte }: { +export const UseGridServiceGetGridRunsKeyFn = ({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, triggeringUser }: { dagId: string; limit?: number; offset?: number; @@ -816,7 +818,9 @@ export const UseGridServiceGetGridRunsKeyFn = ({ dagId, limit, offset, orderBy, runAfterGte?: string; runAfterLt?: string; runAfterLte?: string; -}, queryKey?: Array<unknown>) => [useGridServiceGetGridRunsKey, ...(queryKey ?? [{ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte }])]; + runType?: string[]; + triggeringUser?: string; +}, queryKey?: Array<unknown>) => [useGridServiceGetGridRunsKey, ...(queryKey ?? [{ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, triggeringUser }])]; export type GridServiceGetGridTiSummariesDefaultResponse = Awaited<ReturnType<typeof GridService.getGridTiSummaries>>; export type GridServiceGetGridTiSummariesQueryResult<TData = GridServiceGetGridTiSummariesDefaultResponse, TError = unknown> = UseQueryResult<TData, TError>; export const useGridServiceGetGridTiSummariesKey = "GridServiceGetGridTiSummaries"; 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 1f3e5dc4e3e..75803561cfa 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts @@ -1510,10 +1510,12 @@ export const ensureUseStructureServiceStructureDataData = (queryClient: QueryCli * @param data.runAfterGt * @param data.runAfterLte * @param data.runAfterLt +* @param data.runType +* @param data.triggeringUser SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns GridNodeResponse Successful Response * @throws ApiError */ -export const ensureUseGridServiceGetDagStructureData = (queryClient: QueryClient, { dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte }: { +export const ensureUseGridServiceGetDagStructureData = (queryClient: QueryClient, { dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, triggeringUser }: { dagId: string; limit?: number; offset?: number; @@ -1522,7 +1524,9 @@ export const ensureUseGridServiceGetDagStructureData = (queryClient: QueryClient runAfterGte?: string; runAfterLt?: string; runAfterLte?: string; -}) => queryClient.ensureQueryData({ queryKey: Common.UseGridServiceGetDagStructureKeyFn({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte }), queryFn: () => GridService.getDagStructure({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte }) }); + runType?: string[]; + triggeringUser?: string; +}) => queryClient.ensureQueryData({ queryKey: Common.UseGridServiceGetDagStructureKeyFn({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, triggeringUser }), queryFn: () => GridService.getDagStructure({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, triggeringUser }) }); /** * Get Grid Runs * Get info about a run for the grid. @@ -1535,10 +1539,12 @@ export const ensureUseGridServiceGetDagStructureData = (queryClient: QueryClient * @param data.runAfterGt * @param data.runAfterLte * @param data.runAfterLt +* @param data.runType +* @param data.triggeringUser SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns GridRunsResponse Successful Response * @throws ApiError */ -export const ensureUseGridServiceGetGridRunsData = (queryClient: QueryClient, { dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte }: { +export const ensureUseGridServiceGetGridRunsData = (queryClient: QueryClient, { dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, triggeringUser }: { dagId: string; limit?: number; offset?: number; @@ -1547,7 +1553,9 @@ export const ensureUseGridServiceGetGridRunsData = (queryClient: QueryClient, { runAfterGte?: string; runAfterLt?: string; runAfterLte?: string; -}) => queryClient.ensureQueryData({ queryKey: Common.UseGridServiceGetGridRunsKeyFn({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte }), queryFn: () => GridService.getGridRuns({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte }) }); + runType?: string[]; + triggeringUser?: string; +}) => queryClient.ensureQueryData({ queryKey: Common.UseGridServiceGetGridRunsKeyFn({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, triggeringUser }), queryFn: () => GridService.getGridRuns({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, triggeringUser }) }); /** * Get Grid Ti Summaries * Get states for TIs / "groups" of TIs. 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 304b6e37d17..180eb826210 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts @@ -1510,10 +1510,12 @@ export const prefetchUseStructureServiceStructureData = (queryClient: QueryClien * @param data.runAfterGt * @param data.runAfterLte * @param data.runAfterLt +* @param data.runType +* @param data.triggeringUser SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns GridNodeResponse Successful Response * @throws ApiError */ -export const prefetchUseGridServiceGetDagStructure = (queryClient: QueryClient, { dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte }: { +export const prefetchUseGridServiceGetDagStructure = (queryClient: QueryClient, { dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, triggeringUser }: { dagId: string; limit?: number; offset?: number; @@ -1522,7 +1524,9 @@ export const prefetchUseGridServiceGetDagStructure = (queryClient: QueryClient, runAfterGte?: string; runAfterLt?: string; runAfterLte?: string; -}) => queryClient.prefetchQuery({ queryKey: Common.UseGridServiceGetDagStructureKeyFn({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte }), queryFn: () => GridService.getDagStructure({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte }) }); + runType?: string[]; + triggeringUser?: string; +}) => queryClient.prefetchQuery({ queryKey: Common.UseGridServiceGetDagStructureKeyFn({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, triggeringUser }), queryFn: () => GridService.getDagStructure({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, triggeringUser }) }); /** * Get Grid Runs * Get info about a run for the grid. @@ -1535,10 +1539,12 @@ export const prefetchUseGridServiceGetDagStructure = (queryClient: QueryClient, * @param data.runAfterGt * @param data.runAfterLte * @param data.runAfterLt +* @param data.runType +* @param data.triggeringUser SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns GridRunsResponse Successful Response * @throws ApiError */ -export const prefetchUseGridServiceGetGridRuns = (queryClient: QueryClient, { dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte }: { +export const prefetchUseGridServiceGetGridRuns = (queryClient: QueryClient, { dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, triggeringUser }: { dagId: string; limit?: number; offset?: number; @@ -1547,7 +1553,9 @@ export const prefetchUseGridServiceGetGridRuns = (queryClient: QueryClient, { da runAfterGte?: string; runAfterLt?: string; runAfterLte?: string; -}) => queryClient.prefetchQuery({ queryKey: Common.UseGridServiceGetGridRunsKeyFn({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte }), queryFn: () => GridService.getGridRuns({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte }) }); + runType?: string[]; + triggeringUser?: string; +}) => queryClient.prefetchQuery({ queryKey: Common.UseGridServiceGetGridRunsKeyFn({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, triggeringUser }), queryFn: () => GridService.getGridRuns({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, triggeringUser }) }); /** * Get Grid Ti Summaries * Get states for TIs / "groups" of TIs. 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 f727dcdbd4f..1b133994fec 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts @@ -1510,10 +1510,12 @@ export const useStructureServiceStructureData = <TData = Common.StructureService * @param data.runAfterGt * @param data.runAfterLte * @param data.runAfterLt +* @param data.runType +* @param data.triggeringUser SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns GridNodeResponse Successful Response * @throws ApiError */ -export const useGridServiceGetDagStructure = <TData = Common.GridServiceGetDagStructureDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte }: { +export const useGridServiceGetDagStructure = <TData = Common.GridServiceGetDagStructureDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, triggeringUser }: { dagId: string; limit?: number; offset?: number; @@ -1522,7 +1524,9 @@ export const useGridServiceGetDagStructure = <TData = Common.GridServiceGetDagSt runAfterGte?: string; runAfterLt?: string; runAfterLte?: string; -}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: Common.UseGridServiceGetDagStructureKeyFn({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte }, queryKey), queryFn: () => GridService.getDagStructure({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte }) as TData, ...options }); + runType?: string[]; + triggeringUser?: string; +}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: Common.UseGridServiceGetDagStructureKeyFn({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, triggeringUser }, queryKey), queryFn: () => GridService.getDagStructure({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, triggeringUser }) as TData, ...options }); /** * Get Grid Runs * Get info about a run for the grid. @@ -1535,10 +1539,12 @@ export const useGridServiceGetDagStructure = <TData = Common.GridServiceGetDagSt * @param data.runAfterGt * @param data.runAfterLte * @param data.runAfterLt +* @param data.runType +* @param data.triggeringUser SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns GridRunsResponse Successful Response * @throws ApiError */ -export const useGridServiceGetGridRuns = <TData = Common.GridServiceGetGridRunsDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte }: { +export const useGridServiceGetGridRuns = <TData = Common.GridServiceGetGridRunsDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, triggeringUser }: { dagId: string; limit?: number; offset?: number; @@ -1547,7 +1553,9 @@ export const useGridServiceGetGridRuns = <TData = Common.GridServiceGetGridRunsD runAfterGte?: string; runAfterLt?: string; runAfterLte?: string; -}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: Common.UseGridServiceGetGridRunsKeyFn({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte }, queryKey), queryFn: () => GridService.getGridRuns({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte }) as TData, ...options }); + runType?: string[]; + triggeringUser?: string; +}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: Common.UseGridServiceGetGridRunsKeyFn({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, triggeringUser }, queryKey), queryFn: () => GridService.getGridRuns({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, triggeringUser }) as TData, ...options }); /** * Get Grid Ti Summaries * Get states for TIs / "groups" of TIs. 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 35d89616172..45fa755cb8d 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts @@ -1510,10 +1510,12 @@ export const useStructureServiceStructureDataSuspense = <TData = Common.Structur * @param data.runAfterGt * @param data.runAfterLte * @param data.runAfterLt +* @param data.runType +* @param data.triggeringUser SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns GridNodeResponse Successful Response * @throws ApiError */ -export const useGridServiceGetDagStructureSuspense = <TData = Common.GridServiceGetDagStructureDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte }: { +export const useGridServiceGetDagStructureSuspense = <TData = Common.GridServiceGetDagStructureDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, triggeringUser }: { dagId: string; limit?: number; offset?: number; @@ -1522,7 +1524,9 @@ export const useGridServiceGetDagStructureSuspense = <TData = Common.GridService runAfterGte?: string; runAfterLt?: string; runAfterLte?: string; -}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: Common.UseGridServiceGetDagStructureKeyFn({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte }, queryKey), queryFn: () => GridService.getDagStructure({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte }) as TData, ...options }); + runType?: string[]; + triggeringUser?: string; +}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: Common.UseGridServiceGetDagStructureKeyFn({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, triggeringUser }, queryKey), queryFn: () => GridService.getDagStructure({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, triggeringUser }) as TData, ...options }); /** * Get Grid Runs * Get info about a run for the grid. @@ -1535,10 +1539,12 @@ export const useGridServiceGetDagStructureSuspense = <TData = Common.GridService * @param data.runAfterGt * @param data.runAfterLte * @param data.runAfterLt +* @param data.runType +* @param data.triggeringUser SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns GridRunsResponse Successful Response * @throws ApiError */ -export const useGridServiceGetGridRunsSuspense = <TData = Common.GridServiceGetGridRunsDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte }: { +export const useGridServiceGetGridRunsSuspense = <TData = Common.GridServiceGetGridRunsDefaultResponse, TError = unknown, TQueryKey extends Array<unknown> = unknown[]>({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, triggeringUser }: { dagId: string; limit?: number; offset?: number; @@ -1547,7 +1553,9 @@ export const useGridServiceGetGridRunsSuspense = <TData = Common.GridServiceGetG runAfterGte?: string; runAfterLt?: string; runAfterLte?: string; -}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: Common.UseGridServiceGetGridRunsKeyFn({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte }, queryKey), queryFn: () => GridService.getGridRuns({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte }) as TData, ...options }); + runType?: string[]; + triggeringUser?: string; +}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: Common.UseGridServiceGetGridRunsKeyFn({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, triggeringUser }, queryKey), queryFn: () => GridService.getGridRuns({ dagId, limit, offset, orderBy, runAfterGt, runAfterGte, runAfterLt, runAfterLte, runType, triggeringUser }) as TData, ...options }); /** * Get Grid Ti Summaries * Get states for TIs / "groups" of TIs. 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 99d6bbb7591..1f074a4d5cc 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 @@ -3888,6 +3888,8 @@ export class GridService { * @param data.runAfterGt * @param data.runAfterLte * @param data.runAfterLt + * @param data.runType + * @param data.triggeringUser SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns GridNodeResponse Successful Response * @throws ApiError */ @@ -3905,7 +3907,9 @@ export class GridService { run_after_gte: data.runAfterGte, run_after_gt: data.runAfterGt, run_after_lte: data.runAfterLte, - run_after_lt: data.runAfterLt + run_after_lt: data.runAfterLt, + run_type: data.runType, + triggering_user: data.triggeringUser }, errors: { 400: 'Bad Request', @@ -3927,6 +3931,8 @@ export class GridService { * @param data.runAfterGt * @param data.runAfterLte * @param data.runAfterLt + * @param data.runType + * @param data.triggeringUser SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns GridRunsResponse Successful Response * @throws ApiError */ @@ -3944,7 +3950,9 @@ export class GridService { run_after_gte: data.runAfterGte, run_after_gt: data.runAfterGt, run_after_lte: data.runAfterLte, - run_after_lt: data.runAfterLt + run_after_lt: data.runAfterLt, + run_type: data.runType, + triggering_user: data.triggeringUser }, 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 b2f31c4dce5..c1320c1c635 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 @@ -3176,6 +3176,11 @@ export type GetDagStructureData = { runAfterGte?: string | null; runAfterLt?: string | null; runAfterLte?: string | null; + runType?: Array<(string)>; + /** + * SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. + */ + triggeringUser?: string | null; }; export type GetDagStructureResponse = Array<GridNodeResponse>; @@ -3189,6 +3194,11 @@ export type GetGridRunsData = { runAfterGte?: string | null; runAfterLt?: string | null; runAfterLte?: string | null; + runType?: Array<(string)>; + /** + * SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. + */ + triggeringUser?: string | null; }; export type GetGridRunsResponse = Array<GridRunsResponse>; diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json index ae58ee278e9..defe64cce63 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/common.json @@ -108,7 +108,8 @@ "runAfterFromPlaceholder": "Run After From", "runAfterToPlaceholder": "Run After To", "runIdPlaceholder": "Filter by Run ID", - "taskIdPlaceholder": "Filter by Task ID" + "taskIdPlaceholder": "Filter by Task ID", + "triggeringUserPlaceholder": "Filter by triggering user" }, "logicalDate": "Logical Date", "logout": "Logout", 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 d79001fa71f..ac20004efe8 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx @@ -28,6 +28,7 @@ import { Outlet, useParams } from "react-router-dom"; import { useLocalStorage } from "usehooks-ts"; import { useDagServiceGetDag, useDagWarningServiceListDagWarnings } from "openapi/queries"; +import type { DagRunType } from "openapi/requests/types.gen"; import BackfillBanner from "src/components/Banner/BackfillBanner"; import { SearchDagsButton } from "src/components/SearchDags"; import TriggerDAGButton from "src/components/TriggerDag/TriggerDAGButton"; @@ -59,6 +60,14 @@ export const DetailsLayout = ({ children, error, isLoading, tabs }: Props) => { const panelGroupRef = useRef(null); const [dagView, setDagView] = useLocalStorage<"graph" | "grid">(`dag_view-${dagId}`, defaultDagView); const [limit, setLimit] = useLocalStorage<number>(`dag_runs_limit-${dagId}`, 10); + const [runTypeFilter, setRunTypeFilter] = useLocalStorage<DagRunType | undefined>( + `run_type_filter-${dagId}`, + undefined, + ); + const [triggeringUserFilter, setTriggeringUserFilter] = useLocalStorage<string | undefined>( + `triggering_user_filter-${dagId}`, + undefined, + ); const [showGantt, setShowGantt] = useLocalStorage<boolean>(`show_gantt-${dagId}`, true); const { fitView, getZoom } = useReactFlow(); @@ -123,16 +132,25 @@ export const DetailsLayout = ({ children, error, isLoading, tabs }: Props) => { dagView={dagView} limit={limit} panelGroupRef={panelGroupRef} + runTypeFilter={runTypeFilter} setDagView={setDagView} setLimit={setLimit} + setRunTypeFilter={setRunTypeFilter} setShowGantt={setShowGantt} + setTriggeringUserFilter={setTriggeringUserFilter} showGantt={showGantt} + triggeringUserFilter={triggeringUserFilter} /> {dagView === "graph" ? ( <Graph /> ) : ( <HStack gap={0}> - <Grid limit={limit} showGantt={Boolean(runId) && showGantt} /> + <Grid + limit={limit} + runType={runTypeFilter} + showGantt={Boolean(runId) && showGantt} + triggeringUser={triggeringUserFilter} + /> {showGantt ? <Gantt limit={limit} /> : undefined} </HStack> )} 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 8a56fc47441..dbf48e0d0df 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 @@ -24,7 +24,7 @@ import { useTranslation } from "react-i18next"; import { FiChevronsRight } from "react-icons/fi"; import { Link, useParams } from "react-router-dom"; -import type { GridRunsResponse } from "openapi/requests"; +import type { DagRunType, GridRunsResponse } from "openapi/requests"; import { useOpenGroups } from "src/context/openGroups"; import { useNavigation } from "src/hooks/navigation"; import { useGridRuns } from "src/queries/useGridRuns.ts"; @@ -41,10 +41,12 @@ dayjs.extend(dayjsDuration); type Props = { readonly limit: number; + readonly runType?: DagRunType | undefined; readonly showGantt?: boolean; + readonly triggeringUser?: string | undefined; }; -export const Grid = ({ limit, showGantt }: Props) => { +export const Grid = ({ limit, runType, showGantt, triggeringUser }: Props) => { const { t: translate } = useTranslation("dag"); const gridRef = useRef<HTMLDivElement>(null); @@ -53,7 +55,7 @@ export const Grid = ({ limit, showGantt }: Props) => { const { openGroupIds, toggleGroupId } = useOpenGroups(); const { dagId = "", runId = "" } = useParams(); - const { data: gridRuns, isLoading } = useGridRuns({ limit }); + const { data: gridRuns, isLoading } = useGridRuns({ limit, runType, triggeringUser }); // Check if the selected dag run is inside of the grid response, if not, we'll update the grid filters // Eventually we should redo the api endpoint to make this work better @@ -77,7 +79,7 @@ export const Grid = ({ limit, showGantt }: Props) => { } }, [gridRuns, setHasActiveRun]); - const { data: dagStructure } = useGridStructure({ hasActiveRun, limit }); + const { data: dagStructure } = useGridStructure({ hasActiveRun, limit, runType, triggeringUser }); // calculate dag run bar heights relative to max const max = Math.max.apply( diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx index 34c06f6ab4f..68eff209fe0 100644 --- a/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx +++ b/airflow-core/src/airflow/ui/src/layouts/Details/PanelButtons.tsx @@ -40,10 +40,14 @@ import { MdOutlineAccountTree } from "react-icons/md"; import { useParams } from "react-router-dom"; import { useLocalStorage } from "usehooks-ts"; +import type { DagRunType } from "openapi/requests/types.gen"; import { DagVersionSelect } from "src/components/DagVersionSelect"; import { directionOptions, type Direction } from "src/components/Graph/useGraphLayout"; +import { RunTypeIcon } from "src/components/RunTypeIcon"; +import { SearchBar } from "src/components/SearchBar"; import { Button, Tooltip } from "src/components/ui"; import { Checkbox } from "src/components/ui/Checkbox"; +import { dagRunTypeOptions } from "src/constants/stateOptions"; import { DagRunSelect } from "./DagRunSelect"; import { ToggleGroups } from "./ToggleGroups"; @@ -52,10 +56,14 @@ type Props = { readonly dagView: string; readonly limit: number; readonly panelGroupRef: React.RefObject<{ setLayout?: (layout: Array<number>) => void } & HTMLDivElement>; + readonly runTypeFilter: DagRunType | undefined; readonly setDagView: (x: "graph" | "grid") => void; readonly setLimit: React.Dispatch<React.SetStateAction<number>>; + readonly setRunTypeFilter: React.Dispatch<React.SetStateAction<DagRunType | undefined>>; readonly setShowGantt: React.Dispatch<React.SetStateAction<boolean>>; + readonly setTriggeringUserFilter: React.Dispatch<React.SetStateAction<string | undefined>>; readonly showGantt: boolean; + readonly triggeringUserFilter: string | undefined; }; const getOptions = (translate: (key: string) => string) => @@ -86,10 +94,14 @@ export const PanelButtons = ({ dagView, limit, panelGroupRef, + runTypeFilter, setDagView, setLimit, + setRunTypeFilter, setShowGantt, + setTriggeringUserFilter, showGantt, + triggeringUserFilter, }: Props) => { const { t: translate } = useTranslation(["components", "dag"]); const { dagId = "", runId } = useParams(); @@ -122,6 +134,22 @@ export const PanelButtons = ({ } }; + const handleRunTypeChange = (event: SelectValueChangeDetails<string>) => { + const [val] = event.value; + + if (val === undefined || val === "all") { + setRunTypeFilter(undefined); + } else { + setRunTypeFilter(val as DagRunType); + } + }; + + const handleTriggeringUserChange = (value: string) => { + const trimmedValue = value.trim(); + + setTriggeringUserFilter(trimmedValue === "" ? undefined : trimmedValue); + }; + const handleFocus = (view: string) => { if (panelGroupRef.current) { const panelGroup = panelGroupRef.current; @@ -292,6 +320,64 @@ export const PanelButtons = ({ </Select.Content> </Select.Positioner> </Select.Root> + <Select.Root + // @ts-expect-error The expected option type is incorrect + collection={dagRunTypeOptions} + data-testid="run-type-filter" + onValueChange={handleRunTypeChange} + size="sm" + value={[runTypeFilter ?? "all"]} + > + <Select.Label>{translate("common:dagRun.runType")}</Select.Label> + <Select.Control> + <Select.Trigger> + <Select.ValueText> + {(runTypeFilter ?? "all") === "all" ? ( + translate("dags:filters.allRunTypes") + ) : ( + <Flex gap={1}> + <RunTypeIcon runType={runTypeFilter!} /> + {translate( + dagRunTypeOptions.items.find((item) => item.value === runTypeFilter) + ?.label ?? "", + )} + </Flex> + )} + </Select.ValueText> + </Select.Trigger> + <Select.IndicatorGroup> + <Select.Indicator /> + </Select.IndicatorGroup> + </Select.Control> + <Select.Positioner> + <Select.Content> + {dagRunTypeOptions.items.map((option) => ( + <Select.Item item={option} key={option.value}> + {option.value === "all" ? ( + translate(option.label) + ) : ( + <Flex gap={1}> + <RunTypeIcon runType={option.value as DagRunType} /> + {translate(option.label)} + </Flex> + )} + </Select.Item> + ))} + </Select.Content> + </Select.Positioner> + </Select.Root> + <VStack alignItems="flex-start"> + <Text fontSize="xs" mb={1}> + {translate("common:dagRun.triggeringUser")} + </Text> + <SearchBar + defaultValue={triggeringUserFilter ?? ""} + hideAdvanced + hotkeyDisabled + onChange={handleTriggeringUserChange} + placeHolder={translate("common:filters.triggeringUserPlaceholder")} + /> + </VStack> {shouldShowToggleButtons ? ( <VStack alignItems="flex-start" px={1}> <Checkbox checked={showGantt} onChange={() => setShowGantt(!showGantt)} size="sm"> diff --git a/airflow-core/src/airflow/ui/src/queries/useGridRuns.ts b/airflow-core/src/airflow/ui/src/queries/useGridRuns.ts index 42807a9a7bc..af077b7b1af 100644 --- a/airflow-core/src/airflow/ui/src/queries/useGridRuns.ts +++ b/airflow-core/src/airflow/ui/src/queries/useGridRuns.ts @@ -19,9 +19,18 @@ import { useParams } from "react-router-dom"; import { useGridServiceGetGridRuns } from "openapi/queries"; +import type { DagRunType } from "openapi/requests/types.gen"; import { isStatePending, useAutoRefresh } from "src/utils"; -export const useGridRuns = ({ limit }: { limit: number }) => { +export const useGridRuns = ({ + limit, + runType, + triggeringUser, +}: { + limit: number; + runType?: DagRunType | undefined; + triggeringUser?: string | undefined; +}) => { const { dagId = "" } = useParams(); const defaultRefetchInterval = useAutoRefresh({ dagId }); @@ -31,6 +40,8 @@ export const useGridRuns = ({ limit }: { limit: number }) => { dagId, limit, orderBy: ["-run_after"], + runType: runType ? [runType] : undefined, + triggeringUser: triggeringUser ?? undefined, }, undefined, { diff --git a/airflow-core/src/airflow/ui/src/queries/useGridStructure.ts b/airflow-core/src/airflow/ui/src/queries/useGridStructure.ts index 17db10fffd5..f312b028e67 100644 --- a/airflow-core/src/airflow/ui/src/queries/useGridStructure.ts +++ b/airflow-core/src/airflow/ui/src/queries/useGridStructure.ts @@ -19,14 +19,19 @@ import { useParams } from "react-router-dom"; import { useGridServiceGetDagStructure } from "openapi/queries"; +import type { DagRunType } from "openapi/requests/types.gen"; import { useAutoRefresh } from "src/utils"; export const useGridStructure = ({ hasActiveRun = undefined, limit, + runType, + triggeringUser, }: { hasActiveRun?: boolean; limit?: number; + runType?: DagRunType | undefined; + triggeringUser?: string | undefined; }) => { const { dagId = "" } = useParams(); const refetchInterval = useAutoRefresh({ dagId }); @@ -37,6 +42,8 @@ export const useGridStructure = ({ dagId, limit, orderBy: ["-run_after"], + runType: runType ? [runType] : undefined, + triggeringUser: triggeringUser ?? undefined, }, 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 271d90c154b..32f8f2683f0 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 @@ -156,6 +156,8 @@ def setup(dag_maker, session=None): data_interval=data_interval, **triggered_by_kwargs, ) + # Set specific triggering users for testing filtering (only for manual runs) + run_2.triggering_user_name = "user2" for ti in run_1.task_instances: ti.state = TaskInstanceState.SUCCESS for ti in sorted(run_2.task_instances, key=lambda ti: (ti.task_id, ti.map_index)): @@ -494,6 +496,41 @@ class TestGetGridDataEndpoint: }, ] + @pytest.mark.parametrize( + "endpoint,run_type,expected", + [ + ("runs", "scheduled", [GRID_RUN_1]), + ("runs", "manual", [GRID_RUN_2]), + ("structure", "scheduled", GRID_NODES), + ("structure", "manual", GRID_NODES), + ], + ) + def test_filter_by_run_type(self, session, test_client, endpoint, run_type, expected): + session.commit() + response = test_client.get(f"/grid/{endpoint}/{DAG_ID}?run_type={run_type}") + assert response.status_code == 200 + assert response.json() == expected + + @pytest.mark.parametrize( + "endpoint,triggering_user,expected", + [ + ("runs", "user2", [GRID_RUN_2]), + ("runs", "nonexistent", []), + ("structure", "user2", GRID_NODES), + ], + ) + def test_filter_by_triggering_user(self, session, test_client, endpoint, triggering_user, expected): + session.commit() + response = test_client.get(f"/grid/{endpoint}/{DAG_ID}?triggering_user={triggering_user}") + assert response.status_code == 200 + assert response.json() == expected + + def test_get_grid_runs_filter_by_run_type_and_triggering_user(self, session, test_client): + session.commit() + response = test_client.get(f"/grid/runs/{DAG_ID}?run_type=manual&triggering_user=user2") + assert response.status_code == 200 + assert response.json() == [GRID_RUN_2] + def test_grid_ti_summaries_group(self, session, test_client): run_id = "run_4-1" session.commit()