From 3658043ed84f56d4cff9c1b3645317917dc7e94d Mon Sep 17 00:00:00 2001 From: Dheeraj Turaga Date: Tue, 22 Jul 2025 13:54:51 -0500 Subject: [PATCH 1/4] Add Triggering User search field backend implementation --- .../openapi/v2-rest-api-generated.yaml | 12 ++++++++ .../core_api/routes/public/dag_run.py | 5 ++++ .../airflow/ui/openapi-gen/queries/common.ts | 5 ++-- .../ui/openapi-gen/queries/ensureQueryData.ts | 6 ++-- .../ui/openapi-gen/queries/prefetch.ts | 6 ++-- .../airflow/ui/openapi-gen/queries/queries.ts | 6 ++-- .../ui/openapi-gen/queries/suspense.ts | 6 ++-- .../ui/openapi-gen/requests/services.gen.ts | 4 ++- .../ui/openapi-gen/requests/types.gen.ts | 4 +++ .../ui/public/i18n/locales/en/dags.json | 3 +- .../airflow/ui/src/constants/searchParams.ts | 1 + .../src/airflow/ui/src/pages/DagRuns.tsx | 28 +++++++++++++++++++ 12 files changed, 74 insertions(+), 12 deletions(-) diff --git a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml index 74363c05fe0bc..3a49aec23f667 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml +++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml @@ -2087,6 +2087,18 @@ paths: title: Run Id Pattern description: "SQL LIKE expression \u2014 use `%` / `_` wildcards (e.g. `%customer_%`).\ \ Regular expressions are **not** supported." + - name: triggering_user_name_pattern + 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 Name Pattern + 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/public/dag_run.py b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py index 00f93754baf2d..14bdb96d4a47a 100644 --- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py +++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dag_run.py @@ -343,6 +343,10 @@ def get_dag_runs( session: SessionDep, dag_bag: DagBagDep, run_id_pattern: Annotated[_SearchParam, Depends(search_param_factory(DagRun.run_id, "run_id_pattern"))], + triggering_user_name_pattern: Annotated[ + _SearchParam, + Depends(search_param_factory(DagRun.triggering_user_name, "triggering_user_name_pattern")), + ], ) -> DAGRunCollectionResponse: """ Get all DAG Runs. @@ -370,6 +374,7 @@ def get_dag_runs( run_type, readable_dag_runs_filter, run_id_pattern, + triggering_user_name_pattern, ], order_by=order_by, offset=offset, 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 91fd42bd07909..5caa63bd2e504 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts @@ -140,7 +140,7 @@ export const UseDagRunServiceGetUpstreamAssetEventsKeyFn = ({ dagId, dagRunId }: export type DagRunServiceGetDagRunsDefaultResponse = Awaited>; export type DagRunServiceGetDagRunsQueryResult = UseQueryResult; export const useDagRunServiceGetDagRunsKey = "DagRunServiceGetDagRuns"; -export const UseDagRunServiceGetDagRunsKeyFn = ({ dagId, endDateGte, endDateLte, limit, logicalDateGte, logicalDateLte, offset, orderBy, runAfterGte, runAfterLte, runIdPattern, runType, startDateGte, startDateLte, state, updatedAtGte, updatedAtLte }: { +export const UseDagRunServiceGetDagRunsKeyFn = ({ dagId, endDateGte, endDateLte, limit, logicalDateGte, logicalDateLte, offset, orderBy, runAfterGte, runAfterLte, runIdPattern, runType, startDateGte, startDateLte, state, triggeringUserNamePattern, updatedAtGte, updatedAtLte }: { dagId: string; endDateGte?: string; endDateLte?: string; @@ -156,9 +156,10 @@ export const UseDagRunServiceGetDagRunsKeyFn = ({ dagId, endDateGte, endDateLte, startDateGte?: string; startDateLte?: string; state?: string[]; + triggeringUserNamePattern?: string; updatedAtGte?: string; updatedAtLte?: string; -}, queryKey?: Array) => [useDagRunServiceGetDagRunsKey, ...(queryKey ?? [{ dagId, endDateGte, endDateLte, limit, logicalDateGte, logicalDateLte, offset, orderBy, runAfterGte, runAfterLte, runIdPattern, runType, startDateGte, startDateLte, state, updatedAtGte, updatedAtLte }])]; +}, queryKey?: Array) => [useDagRunServiceGetDagRunsKey, ...(queryKey ?? [{ dagId, endDateGte, endDateLte, limit, logicalDateGte, logicalDateLte, offset, orderBy, runAfterGte, runAfterLte, runIdPattern, runType, startDateGte, startDateLte, state, triggeringUserNamePattern, updatedAtGte, updatedAtLte }])]; export type DagRunServiceWaitDagRunUntilFinishedDefaultResponse = Awaited>; export type DagRunServiceWaitDagRunUntilFinishedQueryResult = UseQueryResult; export const useDagRunServiceWaitDagRunUntilFinishedKey = "DagRunServiceWaitDagRunUntilFinished"; 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 d10b539687bf5..e05f35610dc33 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts @@ -273,10 +273,11 @@ export const ensureUseDagRunServiceGetUpstreamAssetEventsData = (queryClient: Qu * @param data.state * @param data.orderBy * @param data.runIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. +* @param data.triggeringUserNamePattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns DAGRunCollectionResponse Successful Response * @throws ApiError */ -export const ensureUseDagRunServiceGetDagRunsData = (queryClient: QueryClient, { dagId, endDateGte, endDateLte, limit, logicalDateGte, logicalDateLte, offset, orderBy, runAfterGte, runAfterLte, runIdPattern, runType, startDateGte, startDateLte, state, updatedAtGte, updatedAtLte }: { +export const ensureUseDagRunServiceGetDagRunsData = (queryClient: QueryClient, { dagId, endDateGte, endDateLte, limit, logicalDateGte, logicalDateLte, offset, orderBy, runAfterGte, runAfterLte, runIdPattern, runType, startDateGte, startDateLte, state, triggeringUserNamePattern, updatedAtGte, updatedAtLte }: { dagId: string; endDateGte?: string; endDateLte?: string; @@ -292,9 +293,10 @@ export const ensureUseDagRunServiceGetDagRunsData = (queryClient: QueryClient, { startDateGte?: string; startDateLte?: string; state?: string[]; + triggeringUserNamePattern?: string; updatedAtGte?: string; updatedAtLte?: string; -}) => queryClient.ensureQueryData({ queryKey: Common.UseDagRunServiceGetDagRunsKeyFn({ dagId, endDateGte, endDateLte, limit, logicalDateGte, logicalDateLte, offset, orderBy, runAfterGte, runAfterLte, runIdPattern, runType, startDateGte, startDateLte, state, updatedAtGte, updatedAtLte }), queryFn: () => DagRunService.getDagRuns({ dagId, endDateGte, endDateLte, limit, logicalDateGte, logicalDateLte, offset, orderBy, runAfterGte, runAfterLte, runIdPattern, runType, startDateGte, startDateLte, state, updatedAtGte, updatedAtLte }) }); +}) => queryClient.ensureQueryData({ queryKey: Common.UseDagRunServiceGetDagRunsKeyFn({ dagId, endDateGte, endDateLte, limit, logicalDateGte, logicalDateLte, offset, orderBy, runAfterGte, runAfterLte, runIdPattern, runType, startDateGte, startDateLte, state, triggeringUserNamePattern, updatedAtGte, updatedAtLte }), queryFn: () => DagRunService.getDagRuns({ dagId, endDateGte, endDateLte, limit, logicalDateGte, logicalDateLte, offset, orderBy, runAfterGte, runAfterLte, runIdPattern, runType, startDateGte, startDateLte, state, triggeringUserNamePattern, updatedAtGte, updatedAtLte }) }); /** * Experimental: Wait for a dag run to complete, and return task results if requested. * 🚧 This is an experimental endpoint and may change or be removed without notice. 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 2eab4c35b3ef4..10dba6fa0183d 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts @@ -273,10 +273,11 @@ export const prefetchUseDagRunServiceGetUpstreamAssetEvents = (queryClient: Quer * @param data.state * @param data.orderBy * @param data.runIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. +* @param data.triggeringUserNamePattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns DAGRunCollectionResponse Successful Response * @throws ApiError */ -export const prefetchUseDagRunServiceGetDagRuns = (queryClient: QueryClient, { dagId, endDateGte, endDateLte, limit, logicalDateGte, logicalDateLte, offset, orderBy, runAfterGte, runAfterLte, runIdPattern, runType, startDateGte, startDateLte, state, updatedAtGte, updatedAtLte }: { +export const prefetchUseDagRunServiceGetDagRuns = (queryClient: QueryClient, { dagId, endDateGte, endDateLte, limit, logicalDateGte, logicalDateLte, offset, orderBy, runAfterGte, runAfterLte, runIdPattern, runType, startDateGte, startDateLte, state, triggeringUserNamePattern, updatedAtGte, updatedAtLte }: { dagId: string; endDateGte?: string; endDateLte?: string; @@ -292,9 +293,10 @@ export const prefetchUseDagRunServiceGetDagRuns = (queryClient: QueryClient, { d startDateGte?: string; startDateLte?: string; state?: string[]; + triggeringUserNamePattern?: string; updatedAtGte?: string; updatedAtLte?: string; -}) => queryClient.prefetchQuery({ queryKey: Common.UseDagRunServiceGetDagRunsKeyFn({ dagId, endDateGte, endDateLte, limit, logicalDateGte, logicalDateLte, offset, orderBy, runAfterGte, runAfterLte, runIdPattern, runType, startDateGte, startDateLte, state, updatedAtGte, updatedAtLte }), queryFn: () => DagRunService.getDagRuns({ dagId, endDateGte, endDateLte, limit, logicalDateGte, logicalDateLte, offset, orderBy, runAfterGte, runAfterLte, runIdPattern, runType, startDateGte, startDateLte, state, updatedAtGte, updatedAtLte }) }); +}) => queryClient.prefetchQuery({ queryKey: Common.UseDagRunServiceGetDagRunsKeyFn({ dagId, endDateGte, endDateLte, limit, logicalDateGte, logicalDateLte, offset, orderBy, runAfterGte, runAfterLte, runIdPattern, runType, startDateGte, startDateLte, state, triggeringUserNamePattern, updatedAtGte, updatedAtLte }), queryFn: () => DagRunService.getDagRuns({ dagId, endDateGte, endDateLte, limit, logicalDateGte, logicalDateLte, offset, orderBy, runAfterGte, runAfterLte, runIdPattern, runType, startDateGte, startDateLte, state, triggeringUserNamePattern, updatedAtGte, updatedAtLte }) }); /** * Experimental: Wait for a dag run to complete, and return task results if requested. * 🚧 This is an experimental endpoint and may change or be removed without notice. 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 f47a175614451..5721fad7736bf 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts @@ -273,10 +273,11 @@ export const useDagRunServiceGetUpstreamAssetEvents = = unknown[]>({ dagId, endDateGte, endDateLte, limit, logicalDateGte, logicalDateLte, offset, orderBy, runAfterGte, runAfterLte, runIdPattern, runType, startDateGte, startDateLte, state, updatedAtGte, updatedAtLte }: { +export const useDagRunServiceGetDagRuns = = unknown[]>({ dagId, endDateGte, endDateLte, limit, logicalDateGte, logicalDateLte, offset, orderBy, runAfterGte, runAfterLte, runIdPattern, runType, startDateGte, startDateLte, state, triggeringUserNamePattern, updatedAtGte, updatedAtLte }: { dagId: string; endDateGte?: string; endDateLte?: string; @@ -292,9 +293,10 @@ export const useDagRunServiceGetDagRuns = , "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseDagRunServiceGetDagRunsKeyFn({ dagId, endDateGte, endDateLte, limit, logicalDateGte, logicalDateLte, offset, orderBy, runAfterGte, runAfterLte, runIdPattern, runType, startDateGte, startDateLte, state, updatedAtGte, updatedAtLte }, queryKey), queryFn: () => DagRunService.getDagRuns({ dagId, endDateGte, endDateLte, limit, logicalDateGte, logicalDateLte, offset, orderBy, runAfterGte, runAfterLte, runIdPattern, runType, startDateGte, startDateLte, state, updatedAtGte, updatedAtLte }) as TData, ...options }); +}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useQuery({ queryKey: Common.UseDagRunServiceGetDagRunsKeyFn({ dagId, endDateGte, endDateLte, limit, logicalDateGte, logicalDateLte, offset, orderBy, runAfterGte, runAfterLte, runIdPattern, runType, startDateGte, startDateLte, state, triggeringUserNamePattern, updatedAtGte, updatedAtLte }, queryKey), queryFn: () => DagRunService.getDagRuns({ dagId, endDateGte, endDateLte, limit, logicalDateGte, logicalDateLte, offset, orderBy, runAfterGte, runAfterLte, runIdPattern, runType, startDateGte, startDateLte, state, triggeringUserNamePattern, updatedAtGte, updatedAtLte }) as TData, ...options }); /** * Experimental: Wait for a dag run to complete, and return task results if requested. * 🚧 This is an experimental endpoint and may change or be removed without notice. 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 2f9e37e78d6c2..fea0da07a3307 100644 --- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts +++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts @@ -273,10 +273,11 @@ export const useDagRunServiceGetUpstreamAssetEventsSuspense = = unknown[]>({ dagId, endDateGte, endDateLte, limit, logicalDateGte, logicalDateLte, offset, orderBy, runAfterGte, runAfterLte, runIdPattern, runType, startDateGte, startDateLte, state, updatedAtGte, updatedAtLte }: { +export const useDagRunServiceGetDagRunsSuspense = = unknown[]>({ dagId, endDateGte, endDateLte, limit, logicalDateGte, logicalDateLte, offset, orderBy, runAfterGte, runAfterLte, runIdPattern, runType, startDateGte, startDateLte, state, triggeringUserNamePattern, updatedAtGte, updatedAtLte }: { dagId: string; endDateGte?: string; endDateLte?: string; @@ -292,9 +293,10 @@ export const useDagRunServiceGetDagRunsSuspense = , "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseDagRunServiceGetDagRunsKeyFn({ dagId, endDateGte, endDateLte, limit, logicalDateGte, logicalDateLte, offset, orderBy, runAfterGte, runAfterLte, runIdPattern, runType, startDateGte, startDateLte, state, updatedAtGte, updatedAtLte }, queryKey), queryFn: () => DagRunService.getDagRuns({ dagId, endDateGte, endDateLte, limit, logicalDateGte, logicalDateLte, offset, orderBy, runAfterGte, runAfterLte, runIdPattern, runType, startDateGte, startDateLte, state, updatedAtGte, updatedAtLte }) as TData, ...options }); +}, queryKey?: TQueryKey, options?: Omit, "queryKey" | "queryFn">) => useSuspenseQuery({ queryKey: Common.UseDagRunServiceGetDagRunsKeyFn({ dagId, endDateGte, endDateLte, limit, logicalDateGte, logicalDateLte, offset, orderBy, runAfterGte, runAfterLte, runIdPattern, runType, startDateGte, startDateLte, state, triggeringUserNamePattern, updatedAtGte, updatedAtLte }, queryKey), queryFn: () => DagRunService.getDagRuns({ dagId, endDateGte, endDateLte, limit, logicalDateGte, logicalDateLte, offset, orderBy, runAfterGte, runAfterLte, runIdPattern, runType, startDateGte, startDateLte, state, triggeringUserNamePattern, updatedAtGte, updatedAtLte }) as TData, ...options }); /** * Experimental: Wait for a dag run to complete, and return task results if requested. * 🚧 This is an experimental endpoint and may change or be removed without notice. 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 b935e2042366d..7f3aa2095b49d 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 @@ -984,6 +984,7 @@ export class DagRunService { * @param data.state * @param data.orderBy * @param data.runIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. + * @param data.triggeringUserNamePattern SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. * @returns DAGRunCollectionResponse Successful Response * @throws ApiError */ @@ -1010,7 +1011,8 @@ export class DagRunService { run_type: data.runType, state: data.state, order_by: data.orderBy, - run_id_pattern: data.runIdPattern + run_id_pattern: data.runIdPattern, + triggering_user_name_pattern: data.triggeringUserNamePattern }, errors: { 401: 'Unauthorized', 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 06887e2caa236..c1d4367be794a 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 @@ -2206,6 +2206,10 @@ export type GetDagRunsData = { startDateGte?: string | null; startDateLte?: string | null; state?: Array<(string)>; + /** + * SQL LIKE expression — use `%` / `_` wildcards (e.g. `%customer_%`). Regular expressions are **not** supported. + */ + triggeringUserNamePattern?: string | null; updatedAtGte?: string | null; updatedAtLte?: string | null; }; 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 6705e10196206..1367ac6a929f8 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 @@ -20,7 +20,8 @@ "all": "All", "paused": "Paused" }, - "runIdPatternFilter": "Search Dag Runs" + "runIdPatternFilter": "Search Dag Runs", + "triggeringUserNameFilter": "Search by Triggering User" }, "ownerLink": "Owner link for {{owner}}", "runAndTaskActions": { diff --git a/airflow-core/src/airflow/ui/src/constants/searchParams.ts b/airflow-core/src/airflow/ui/src/constants/searchParams.ts index e5e05cec60060..b2a218c719552 100644 --- a/airflow-core/src/airflow/ui/src/constants/searchParams.ts +++ b/airflow-core/src/airflow/ui/src/constants/searchParams.ts @@ -36,6 +36,7 @@ export enum SearchParamsKeys { STATE = "state", TAGS = "tags", TAGS_MATCH_MODE = "tags_match_mode", + TRIGGERING_USER_NAME_PATTERN = "triggering_user_name_pattern", TRY_NUMBER = "try_number", VERSION_NUMBER = "version_number", } diff --git a/airflow-core/src/airflow/ui/src/pages/DagRuns.tsx b/airflow-core/src/airflow/ui/src/pages/DagRuns.tsx index 1ea3f965a07a6..c413cb7cf3f56 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagRuns.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagRuns.tsx @@ -52,6 +52,7 @@ const { RUN_TYPE: RUN_TYPE_PARAM, START_DATE: START_DATE_PARAM, STATE: STATE_PARAM, + TRIGGERING_USER_NAME_PATTERN: TRIGGERING_USER_NAME_PATTERN_PARAM, }: SearchParamsKeysType = SearchParamsKeys; const runColumns = (translate: TFunction, dagId?: string): Array> => [ @@ -168,6 +169,7 @@ export const DagRuns = () => { const filteredState = searchParams.get(STATE_PARAM); const filteredType = searchParams.get(RUN_TYPE_PARAM); const filteredRunIdPattern = searchParams.get(RUN_ID_PATTERN_PARAM); + const filteredTriggeringUserNamePattern = searchParams.get(TRIGGERING_USER_NAME_PATTERN_PARAM); const startDate = searchParams.get(START_DATE_PARAM); const endDate = searchParams.get(END_DATE_PARAM); @@ -184,6 +186,7 @@ export const DagRuns = () => { runType: filteredType === null ? undefined : [filteredType], startDateGte: startDate ?? undefined, state: filteredState === null ? undefined : [filteredState], + triggeringUserNamePattern: filteredTriggeringUserNamePattern ?? undefined, }, undefined, { @@ -245,6 +248,22 @@ export const DagRuns = () => { [pagination, searchParams, setSearchParams, setTableURLState, sorting], ); + const handleTriggeringUserNamePatternChange = useCallback( + (value: string) => { + if (value === "") { + searchParams.delete(TRIGGERING_USER_NAME_PATTERN_PARAM); + } else { + searchParams.set(TRIGGERING_USER_NAME_PATTERN_PARAM, value); + } + setTableURLState({ + pagination: { ...pagination, pageIndex: 0 }, + sorting, + }); + setSearchParams(searchParams); + }, + [pagination, searchParams, setSearchParams, setTableURLState, sorting], + ); + return ( <> @@ -257,6 +276,15 @@ export const DagRuns = () => { placeHolder={translate("dags:filters.runIdPatternFilter")} /> + + + Date: Wed, 23 Jul 2025 23:09:07 -0500 Subject: [PATCH 2/4] Add tests --- .../core_api/routes/public/test_dag_run.py | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py index dbe55a92e7893..6729dab0b027b 100644 --- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py +++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dag_run.py @@ -577,6 +577,179 @@ def test_invalid_state(self, test_client): ) +class TestTriggeringUserNamePatternFilter: + """Test class specifically for triggering_user_name_pattern filtering functionality.""" + + @pytest.fixture(autouse=True) + @provide_session + def setup_triggering_user_test_data(self, dag_maker, session=None): + """Set up test data with different triggering user names.""" + clear_db_connections() + clear_db_runs() + clear_db_dags() + clear_db_serialized_dags() + clear_db_logs() + + # Create DAG with some tasks + with dag_maker("test_triggering_user_dag", schedule=None, start_date=START_DATE1, serialized=True): + EmptyOperator(task_id="task_1") + + # Create DAG runs with different triggering user names + self.dag_run_user_alice = dag_maker.create_dagrun( + run_id="run_user_alice", + state=DagRunState.SUCCESS, + run_type=DagRunType.MANUAL, + triggered_by=DagRunTriggeredByType.UI, + logical_date=LOGICAL_DATE1, + ) + # Set triggering_user_name manually + self.dag_run_user_alice.triggering_user_name = "alice" + + self.dag_run_user_bob = dag_maker.create_dagrun( + run_id="run_user_bob", + state=DagRunState.SUCCESS, + run_type=DagRunType.MANUAL, + triggered_by=DagRunTriggeredByType.UI, + logical_date=LOGICAL_DATE2, + ) + self.dag_run_user_bob.triggering_user_name = "bob_admin" + + self.dag_run_user_service = dag_maker.create_dagrun( + run_id="run_service_account", + state=DagRunState.SUCCESS, + run_type=DagRunType.MANUAL, + triggered_by=DagRunTriggeredByType.REST_API, + logical_date=LOGICAL_DATE3, + ) + self.dag_run_user_service.triggering_user_name = "service_account" + + self.dag_run_no_user = dag_maker.create_dagrun( + run_id="run_no_user", + state=DagRunState.SUCCESS, + run_type=DagRunType.SCHEDULED, + triggered_by=DagRunTriggeredByType.TIMETABLE, + logical_date=LOGICAL_DATE4, + ) + # This one has no triggering_user_name (None) + self.dag_run_no_user.triggering_user_name = None + + dag_maker.sync_dagbag_to_db() + session.merge(dag_maker.dag_model) + session.commit() + + @pytest.mark.parametrize( + "query_params, expected_run_ids", + [ + # Test exact match + ({"triggering_user_name_pattern": "alice"}, ["run_user_alice"]), + ({"triggering_user_name_pattern": "bob_admin"}, ["run_user_bob"]), + ({"triggering_user_name_pattern": "service_account"}, ["run_service_account"]), + # Test prefix wildcard patterns + ({"triggering_user_name_pattern": "alice%"}, ["run_user_alice"]), + ({"triggering_user_name_pattern": "bob%"}, ["run_user_bob"]), + ({"triggering_user_name_pattern": "service%"}, ["run_service_account"]), + # Test suffix wildcard patterns + ({"triggering_user_name_pattern": "%alice"}, ["run_user_alice"]), + ({"triggering_user_name_pattern": "%bob"}, ["run_user_bob"]), + ({"triggering_user_name_pattern": "%_account"}, ["run_service_account"]), + # Test contains wildcard patterns + ({"triggering_user_name_pattern": "%service%"}, ["run_service_account"]), + ({"triggering_user_name_pattern": "%bob%"}, ["run_user_bob"]), + ({"triggering_user_name_pattern": "%alice%"}, ["run_user_alice"]), + # Test more wildcard patterns + ({"triggering_user_name_pattern": "serv%_account"}, ["run_service_account"]), + ({"triggering_user_name_pattern": "bob%min"}, ["run_user_bob"]), + # Test no matches + ({"triggering_user_name_pattern": "nonexistent"}, []), + ({"triggering_user_name_pattern": "xyz%"}, []), + # Test empty pattern (should return all runs with non-null triggering_user_name) + ( + {"triggering_user_name_pattern": "%"}, + ["run_user_alice", "run_user_bob", "run_service_account"], + ), + ], + ) + def test_triggering_user_name_pattern_filter(self, test_client, query_params, expected_run_ids): + """Test that triggering_user_name_pattern filter works correctly with SQL LIKE patterns.""" + response = test_client.get("/dags/test_triggering_user_dag/dagRuns", params=query_params) + assert response.status_code == 200 + + body = response.json() + actual_run_ids = [run["dag_run_id"] for run in body["dag_runs"]] + + # Sort both lists to ensure consistent comparison + assert sorted(actual_run_ids) == sorted(expected_run_ids) + + def test_triggering_user_name_pattern_with_other_filters(self, test_client): + """Test that triggering_user_name_pattern works in combination with other filters.""" + # Test combining with state filter + query_params = {"triggering_user_name_pattern": "%bob%", "state": DagRunState.SUCCESS.value} + response = test_client.get("/dags/test_triggering_user_dag/dagRuns", params=query_params) + assert response.status_code == 200 + + body = response.json() + actual_run_ids = [run["dag_run_id"] for run in body["dag_runs"]] + assert actual_run_ids == ["run_user_bob"] + + def test_triggering_user_name_pattern_all_dags(self, test_client): + """Test triggering_user_name_pattern filter with dag_id='~' (all DAGs).""" + response = test_client.get("/dags/~/dagRuns", params={"triggering_user_name_pattern": "alice"}) + assert response.status_code == 200 + + body = response.json() + # Should find the alice run from our test DAG + alice_runs = [run for run in body["dag_runs"] if run["dag_run_id"] == "run_user_alice"] + assert len(alice_runs) == 1 + assert alice_runs[0]["triggering_user_name"] == "alice" + + def test_triggering_user_name_pattern_case_behavior(self, test_client): + """Test the behavior of triggering_user_name_pattern filter with different cases.""" + # Test exact match with correct case - should match + response = test_client.get( + "/dags/test_triggering_user_dag/dagRuns", params={"triggering_user_name_pattern": "alice"} + ) + assert response.status_code == 200 + body = response.json() + assert len(body["dag_runs"]) == 1 + assert body["dag_runs"][0]["dag_run_id"] == "run_user_alice" + assert body["dag_runs"][0]["triggering_user_name"] == "alice" + + # Test different case - behavior may depend on database collation + # This test documents the actual behavior rather than enforcing case sensitivity + response = test_client.get( + "/dags/test_triggering_user_dag/dagRuns", params={"triggering_user_name_pattern": "ALICE"} + ) + assert response.status_code == 200 + body = response.json() + # Note: This may return 0 or 1 results depending on database collation settings + # The important thing is that the filter works and doesn't crash + assert len(body["dag_runs"]) >= 0 + + def test_triggering_user_name_pattern_underscore_wildcard(self, test_client): + """Test underscore wildcard patterns separately for better debugging.""" + # Test underscore wildcard with alice (5 chars: a-l-i-c-e) + response = test_client.get( + "/dags/test_triggering_user_dag/dagRuns", params={"triggering_user_name_pattern": "alic_"} + ) + assert response.status_code == 200 + body = response.json() + # Should match "alice" where _ matches 'e' + expected_usernames = [run["triggering_user_name"] for run in body["dag_runs"]] + if len(body["dag_runs"]) > 0: + assert "alice" in expected_usernames + + # Test a different underscore pattern + response = test_client.get( + "/dags/test_triggering_user_dag/dagRuns", params={"triggering_user_name_pattern": "al___"} + ) + assert response.status_code == 200 + body = response.json() + # Should match "alice" where ___ matches 'ice' + expected_usernames = [run["triggering_user_name"] for run in body["dag_runs"]] + if len(body["dag_runs"]) > 0: + assert "alice" in expected_usernames + + class TestListDagRunsBatch: @pytest.mark.usefixtures("configure_git_connection_for_dag_bundle") def test_list_dag_runs_return_200(self, test_client, session): From 4cb7c3aed2034baa33ac3d3e5b83b5422f29945d Mon Sep 17 00:00:00 2001 From: Dheeraj Turaga Date: Fri, 25 Jul 2025 23:54:42 -0500 Subject: [PATCH 3/4] Pierre's Suggestions --- .../airflow/ui/src/components/SearchBar.tsx | 2 +- .../src/airflow/ui/src/pages/DagRuns.tsx | 6 +- .../core_api/routes/public/test_dag_run.py | 202 +++--------------- 3 files changed, 32 insertions(+), 178 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/components/SearchBar.tsx b/airflow-core/src/airflow/ui/src/components/SearchBar.tsx index ca3d0b552cd45..f5b0607ccf379 100644 --- a/airflow-core/src/airflow/ui/src/components/SearchBar.tsx +++ b/airflow-core/src/airflow/ui/src/components/SearchBar.tsx @@ -32,7 +32,7 @@ const debounceDelay = 200; type Props = { readonly buttonProps?: ButtonProps; readonly defaultValue: string; - readonly groupProps?: InputGroupProps; + readonly groupProps?: Omit; readonly hideAdvanced?: boolean; readonly hotkeyDisabled?: boolean; readonly onChange: (value: string) => void; diff --git a/airflow-core/src/airflow/ui/src/pages/DagRuns.tsx b/airflow-core/src/airflow/ui/src/pages/DagRuns.tsx index c413cb7cf3f56..0da4777e9a689 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagRuns.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagRuns.tsx @@ -276,9 +276,13 @@ export const DagRuns = () => { placeHolder={translate("dags:filters.runIdPatternFilter")} /> - + = 0 - - def test_triggering_user_name_pattern_underscore_wildcard(self, test_client): - """Test underscore wildcard patterns separately for better debugging.""" - # Test underscore wildcard with alice (5 chars: a-l-i-c-e) - response = test_client.get( - "/dags/test_triggering_user_dag/dagRuns", params={"triggering_user_name_pattern": "alic_"} - ) - assert response.status_code == 200 - body = response.json() - # Should match "alice" where _ matches 'e' - expected_usernames = [run["triggering_user_name"] for run in body["dag_runs"]] - if len(body["dag_runs"]) > 0: - assert "alice" in expected_usernames - - # Test a different underscore pattern - response = test_client.get( - "/dags/test_triggering_user_dag/dagRuns", params={"triggering_user_name_pattern": "al___"} - ) - assert response.status_code == 200 - body = response.json() - # Should match "alice" where ___ matches 'ice' - expected_usernames = [run["triggering_user_name"] for run in body["dag_runs"]] - if len(body["dag_runs"]) > 0: - assert "alice" in expected_usernames - - class TestListDagRunsBatch: @pytest.mark.usefixtures("configure_git_connection_for_dag_bundle") def test_list_dag_runs_return_200(self, test_client, session): From d37479cf4608585e2966b71e19aedce968a1a8c6 Mon Sep 17 00:00:00 2001 From: Dheeraj Turaga Date: Fri, 1 Aug 2025 22:28:40 -0500 Subject: [PATCH 4/4] Cleanup search box size logic --- airflow-core/src/airflow/ui/src/components/SearchBar.tsx | 2 +- airflow-core/src/airflow/ui/src/pages/DagRuns.tsx | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/components/SearchBar.tsx b/airflow-core/src/airflow/ui/src/components/SearchBar.tsx index f5b0607ccf379..ca3d0b552cd45 100644 --- a/airflow-core/src/airflow/ui/src/components/SearchBar.tsx +++ b/airflow-core/src/airflow/ui/src/components/SearchBar.tsx @@ -32,7 +32,7 @@ const debounceDelay = 200; type Props = { readonly buttonProps?: ButtonProps; readonly defaultValue: string; - readonly groupProps?: Omit; + readonly groupProps?: InputGroupProps; readonly hideAdvanced?: boolean; readonly hotkeyDisabled?: boolean; readonly onChange: (value: string) => void; diff --git a/airflow-core/src/airflow/ui/src/pages/DagRuns.tsx b/airflow-core/src/airflow/ui/src/pages/DagRuns.tsx index 0da4777e9a689..c413cb7cf3f56 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagRuns.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagRuns.tsx @@ -276,13 +276,9 @@ export const DagRuns = () => { placeHolder={translate("dags:filters.runIdPatternFilter")} /> - +