This is an automated email from the ASF dual-hosted git repository.

vincbeck 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 9198acb15e8 UI: Add team filter to Human-in-the-loop task instances 
listing (#73726)
9198acb15e8 is described below

commit 9198acb15e8075e89414da81c955cfcbb5c479c7
Author: Vincent <[email protected]>
AuthorDate: Tue Sep 29 14:12:38 2026 -0400

    UI: Add team filter to Human-in-the-loop task instances listing (#73726)
    
    In multi-team deployments the Required Actions page could not be scoped by
    team, so operators had no way to focus on the required actions their team
    owns. Expose a team filter on the listing so required actions can be
    narrowed to one or more owning teams.
---
 .../core_api/openapi/v2-rest-api-generated.yaml    |  8 +++
 .../api_fastapi/core_api/routes/public/hitl.py     |  4 ++
 .../src/airflow/ui/openapi-gen/queries/common.ts   |  5 +-
 .../ui/openapi-gen/queries/ensureQueryData.ts      |  6 ++-
 .../src/airflow/ui/openapi-gen/queries/prefetch.ts |  6 ++-
 .../src/airflow/ui/openapi-gen/queries/queries.ts  |  6 ++-
 .../src/airflow/ui/openapi-gen/queries/suspense.ts |  6 ++-
 .../ui/openapi-gen/requests/services.gen.ts        |  2 +
 .../airflow/ui/openapi-gen/requests/types.gen.ts   |  1 +
 .../ui/src/pages/HITLTaskInstances/HITLFilters.tsx |  6 +++
 .../HITLTaskInstances/HITLTaskInstances.test.tsx   | 62 +++++++++++++++++++++-
 .../pages/HITLTaskInstances/HITLTaskInstances.tsx  | 21 ++++++++
 .../core_api/routes/public/test_hitl.py            | 48 ++++++++++++++++-
 13 files changed, 169 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 22f1afe6903..1ca26d0f9c2 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
@@ -11092,6 +11092,14 @@ paths:
           items:
             type: string
           title: State
+      - name: teams
+        in: query
+        required: false
+        schema:
+          type: array
+          items:
+            type: string
+          title: Teams
       - name: response_received
         in: query
         required: false
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/hitl.py 
b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/hitl.py
index a77baedd9a1..25862ab527b 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/hitl.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/hitl.py
@@ -43,7 +43,9 @@ from airflow.api_fastapi.common.parameters import (
     QueryTIStateFilter,
     RangeFilter,
     SortParam,
+    _DagIdTeamsFilter,
     datetime_range_filter_factory,
+    teams_filter_factory,
 )
 from airflow.api_fastapi.common.router import AirflowRouter
 from airflow.api_fastapi.core_api.datamodels.hitl import (
@@ -344,6 +346,7 @@ def get_hitl_details(
     task_id_prefix_pattern: QueryHITLDetailTaskIdPrefixPatternSearch,
     map_index: QueryHITLDetailMapIndexFilter,
     ti_state: QueryTIStateFilter,
+    teams: Annotated[_DagIdTeamsFilter, 
Depends(teams_filter_factory(TI.dag_id))],
     # hitl detail related filter
     response_received: QueryHITLDetailResponseReceivedFilter,
     responded_by_user_id: QueryHITLDetailRespondedUserIdFilter,
@@ -383,6 +386,7 @@ def get_hitl_details(
             task_id_prefix_pattern,
             map_index,
             ti_state,
+            teams,
             # hitl detail related filter
             response_received,
             responded_by_user_id,
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 e1de3ccdd0d..cd883bd0d9d 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
@@ -742,7 +742,7 @@ export const 
UseTaskInstanceServiceGetHitlDetailTryDetailKeyFn = ({ dagId, dagRu
 export type TaskInstanceServiceGetHitlDetailsDefaultResponse = 
Awaited<ReturnType<typeof TaskInstanceService.getHitlDetails>>;
 export type TaskInstanceServiceGetHitlDetailsQueryResult<TData = 
TaskInstanceServiceGetHitlDetailsDefaultResponse, TError = unknown> = 
UseQueryResult<TData, TError>;
 export const useTaskInstanceServiceGetHitlDetailsKey = 
"TaskInstanceServiceGetHitlDetails";
-export const UseTaskInstanceServiceGetHitlDetailsKeyFn = ({ bodySearch, 
createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, 
dagIdPrefixPattern, dagRunId, limit, mapIndex, offset, orderBy, 
respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, 
taskId, taskIdPattern, taskIdPrefixPattern }: {
+export const UseTaskInstanceServiceGetHitlDetailsKeyFn = ({ bodySearch, 
createdAtGt, createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, 
dagIdPrefixPattern, dagRunId, limit, mapIndex, offset, orderBy, 
respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, 
taskId, taskIdPattern, taskIdPrefixPattern, teams }: {
   bodySearch?: string;
   createdAtGt?: string;
   createdAtGte?: string;
@@ -764,7 +764,8 @@ export const UseTaskInstanceServiceGetHitlDetailsKeyFn = ({ 
bodySearch, createdA
   taskId?: string;
   taskIdPattern?: string;
   taskIdPrefixPattern?: string;
-}, queryKey?: Array<unknown>) => [useTaskInstanceServiceGetHitlDetailsKey, 
...(queryKey ?? [{ bodySearch, createdAtGt, createdAtGte, createdAtLt, 
createdAtLte, dagId, dagIdPattern, dagIdPrefixPattern, dagRunId, limit, 
mapIndex, offset, orderBy, respondedByUserId, respondedByUserName, 
responseReceived, state, subjectSearch, taskId, taskIdPattern, 
taskIdPrefixPattern }])];
+  teams?: string[];
+}, queryKey?: Array<unknown>) => [useTaskInstanceServiceGetHitlDetailsKey, 
...(queryKey ?? [{ bodySearch, createdAtGt, createdAtGte, createdAtLt, 
createdAtLte, dagId, dagIdPattern, dagIdPrefixPattern, dagRunId, limit, 
mapIndex, offset, orderBy, respondedByUserId, respondedByUserName, 
responseReceived, state, subjectSearch, taskId, taskIdPattern, 
taskIdPrefixPattern, teams }])];
 export type ImportErrorServiceGetImportErrorDefaultResponse = 
Awaited<ReturnType<typeof ImportErrorService.getImportError>>;
 export type ImportErrorServiceGetImportErrorQueryResult<TData = 
ImportErrorServiceGetImportErrorDefaultResponse, TError = unknown> = 
UseQueryResult<TData, TError>;
 export const useImportErrorServiceGetImportErrorKey = 
"ImportErrorServiceGetImportError";
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 58c12d2a670..768999e3216 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
@@ -1471,6 +1471,7 @@ export const 
ensureUseTaskInstanceServiceGetHitlDetailTryDetailData = (queryClie
 * @param data.taskIdPrefixPattern Case-sensitive, index-friendly prefix match. 
See "Filtering with pattern parameters".
 * @param data.mapIndex
 * @param data.state
+* @param data.teams
 * @param data.responseReceived
 * @param data.respondedByUserId
 * @param data.respondedByUserName
@@ -1483,7 +1484,7 @@ export const 
ensureUseTaskInstanceServiceGetHitlDetailTryDetailData = (queryClie
 * @returns HITLDetailCollection Successful Response
 * @throws ApiError
 */
-export const ensureUseTaskInstanceServiceGetHitlDetailsData = (queryClient: 
QueryClient, { bodySearch, createdAtGt, createdAtGte, createdAtLt, 
createdAtLte, dagId, dagIdPattern, dagIdPrefixPattern, dagRunId, limit, 
mapIndex, offset, orderBy, respondedByUserId, respondedByUserName, 
responseReceived, state, subjectSearch, taskId, taskIdPattern, 
taskIdPrefixPattern }: {
+export const ensureUseTaskInstanceServiceGetHitlDetailsData = (queryClient: 
QueryClient, { bodySearch, createdAtGt, createdAtGte, createdAtLt, 
createdAtLte, dagId, dagIdPattern, dagIdPrefixPattern, dagRunId, limit, 
mapIndex, offset, orderBy, respondedByUserId, respondedByUserName, 
responseReceived, state, subjectSearch, taskId, taskIdPattern, 
taskIdPrefixPattern, teams }: {
   bodySearch?: string;
   createdAtGt?: string;
   createdAtGte?: string;
@@ -1505,7 +1506,8 @@ export const 
ensureUseTaskInstanceServiceGetHitlDetailsData = (queryClient: Quer
   taskId?: string;
   taskIdPattern?: string;
   taskIdPrefixPattern?: string;
-}) => queryClient.ensureQueryData({ queryKey: 
Common.UseTaskInstanceServiceGetHitlDetailsKeyFn({ bodySearch, createdAtGt, 
createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, 
dagIdPrefixPattern, dagRunId, limit, mapIndex, offset, orderBy, 
respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, 
taskId, taskIdPattern, taskIdPrefixPattern }), queryFn: () => 
TaskInstanceService.getHitlDetails({ bodySearch, createdAtGt, createdAtGte, 
createdAtLt, createdAtLte, [...]
+  teams?: string[];
+}) => queryClient.ensureQueryData({ queryKey: 
Common.UseTaskInstanceServiceGetHitlDetailsKeyFn({ bodySearch, createdAtGt, 
createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, 
dagIdPrefixPattern, dagRunId, limit, mapIndex, offset, orderBy, 
respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, 
taskId, taskIdPattern, taskIdPrefixPattern, teams }), queryFn: () => 
TaskInstanceService.getHitlDetails({ bodySearch, createdAtGt, createdAtGte, 
createdAtLt, create [...]
 /**
 * Get Import Error
 * Get an import error.
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 b04f0e549d1..0b991dce2bd 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
@@ -1471,6 +1471,7 @@ export const 
prefetchUseTaskInstanceServiceGetHitlDetailTryDetail = (queryClient
 * @param data.taskIdPrefixPattern Case-sensitive, index-friendly prefix match. 
See "Filtering with pattern parameters".
 * @param data.mapIndex
 * @param data.state
+* @param data.teams
 * @param data.responseReceived
 * @param data.respondedByUserId
 * @param data.respondedByUserName
@@ -1483,7 +1484,7 @@ export const 
prefetchUseTaskInstanceServiceGetHitlDetailTryDetail = (queryClient
 * @returns HITLDetailCollection Successful Response
 * @throws ApiError
 */
-export const prefetchUseTaskInstanceServiceGetHitlDetails = (queryClient: 
QueryClient, { bodySearch, createdAtGt, createdAtGte, createdAtLt, 
createdAtLte, dagId, dagIdPattern, dagIdPrefixPattern, dagRunId, limit, 
mapIndex, offset, orderBy, respondedByUserId, respondedByUserName, 
responseReceived, state, subjectSearch, taskId, taskIdPattern, 
taskIdPrefixPattern }: {
+export const prefetchUseTaskInstanceServiceGetHitlDetails = (queryClient: 
QueryClient, { bodySearch, createdAtGt, createdAtGte, createdAtLt, 
createdAtLte, dagId, dagIdPattern, dagIdPrefixPattern, dagRunId, limit, 
mapIndex, offset, orderBy, respondedByUserId, respondedByUserName, 
responseReceived, state, subjectSearch, taskId, taskIdPattern, 
taskIdPrefixPattern, teams }: {
   bodySearch?: string;
   createdAtGt?: string;
   createdAtGte?: string;
@@ -1505,7 +1506,8 @@ export const prefetchUseTaskInstanceServiceGetHitlDetails 
= (queryClient: QueryC
   taskId?: string;
   taskIdPattern?: string;
   taskIdPrefixPattern?: string;
-}) => queryClient.prefetchQuery({ queryKey: 
Common.UseTaskInstanceServiceGetHitlDetailsKeyFn({ bodySearch, createdAtGt, 
createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, 
dagIdPrefixPattern, dagRunId, limit, mapIndex, offset, orderBy, 
respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, 
taskId, taskIdPattern, taskIdPrefixPattern }), queryFn: () => 
TaskInstanceService.getHitlDetails({ bodySearch, createdAtGt, createdAtGte, 
createdAtLt, createdAtLte, d [...]
+  teams?: string[];
+}) => queryClient.prefetchQuery({ queryKey: 
Common.UseTaskInstanceServiceGetHitlDetailsKeyFn({ bodySearch, createdAtGt, 
createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, 
dagIdPrefixPattern, dagRunId, limit, mapIndex, offset, orderBy, 
respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, 
taskId, taskIdPattern, taskIdPrefixPattern, teams }), queryFn: () => 
TaskInstanceService.getHitlDetails({ bodySearch, createdAtGt, createdAtGte, 
createdAtLt, createdA [...]
 /**
 * Get Import Error
 * Get an import error.
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 df1286bea25..92593cc4a6d 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
@@ -1471,6 +1471,7 @@ export const useTaskInstanceServiceGetHitlDetailTryDetail 
= <TData = Common.Task
 * @param data.taskIdPrefixPattern Case-sensitive, index-friendly prefix match. 
See "Filtering with pattern parameters".
 * @param data.mapIndex
 * @param data.state
+* @param data.teams
 * @param data.responseReceived
 * @param data.respondedByUserId
 * @param data.respondedByUserName
@@ -1483,7 +1484,7 @@ export const useTaskInstanceServiceGetHitlDetailTryDetail 
= <TData = Common.Task
 * @returns HITLDetailCollection Successful Response
 * @throws ApiError
 */
-export const useTaskInstanceServiceGetHitlDetails = <TData = 
Common.TaskInstanceServiceGetHitlDetailsDefaultResponse, TError = unknown, 
TQueryKey extends Array<unknown> = unknown[]>({ bodySearch, createdAtGt, 
createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, 
dagIdPrefixPattern, dagRunId, limit, mapIndex, offset, orderBy, 
respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, 
taskId, taskIdPattern, taskIdPrefixPattern }: {
+export const useTaskInstanceServiceGetHitlDetails = <TData = 
Common.TaskInstanceServiceGetHitlDetailsDefaultResponse, TError = unknown, 
TQueryKey extends Array<unknown> = unknown[]>({ bodySearch, createdAtGt, 
createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, 
dagIdPrefixPattern, dagRunId, limit, mapIndex, offset, orderBy, 
respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, 
taskId, taskIdPattern, taskIdPrefixPattern, teams }: {
   bodySearch?: string;
   createdAtGt?: string;
   createdAtGte?: string;
@@ -1505,7 +1506,8 @@ export const useTaskInstanceServiceGetHitlDetails = 
<TData = Common.TaskInstance
   taskId?: string;
   taskIdPattern?: string;
   taskIdPrefixPattern?: string;
-}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: 
Common.UseTaskInstanceServiceGetHitlDetailsKeyFn({ bodySearch, createdAtGt, 
createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, 
dagIdPrefixPattern, dagRunId, limit, mapIndex, offset, orderBy, 
respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, 
taskId, taskIdPattern, taskIdPrefixPattern }, queryKey), queryFn: () => Ta [...]
+  teams?: string[];
+}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: 
Common.UseTaskInstanceServiceGetHitlDetailsKeyFn({ bodySearch, createdAtGt, 
createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, 
dagIdPrefixPattern, dagRunId, limit, mapIndex, offset, orderBy, 
respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, 
taskId, taskIdPattern, taskIdPrefixPattern, teams }, queryKey), queryFn: ( [...]
 /**
 * Get Import Error
 * Get an import error.
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 31da0a3e582..a7c41344ba9 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
@@ -1471,6 +1471,7 @@ export const 
useTaskInstanceServiceGetHitlDetailTryDetailSuspense = <TData = Com
 * @param data.taskIdPrefixPattern Case-sensitive, index-friendly prefix match. 
See "Filtering with pattern parameters".
 * @param data.mapIndex
 * @param data.state
+* @param data.teams
 * @param data.responseReceived
 * @param data.respondedByUserId
 * @param data.respondedByUserName
@@ -1483,7 +1484,7 @@ export const 
useTaskInstanceServiceGetHitlDetailTryDetailSuspense = <TData = Com
 * @returns HITLDetailCollection Successful Response
 * @throws ApiError
 */
-export const useTaskInstanceServiceGetHitlDetailsSuspense = <TData = 
Common.TaskInstanceServiceGetHitlDetailsDefaultResponse, TError = unknown, 
TQueryKey extends Array<unknown> = unknown[]>({ bodySearch, createdAtGt, 
createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, 
dagIdPrefixPattern, dagRunId, limit, mapIndex, offset, orderBy, 
respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, 
taskId, taskIdPattern, taskIdPrefixPattern }: {
+export const useTaskInstanceServiceGetHitlDetailsSuspense = <TData = 
Common.TaskInstanceServiceGetHitlDetailsDefaultResponse, TError = unknown, 
TQueryKey extends Array<unknown> = unknown[]>({ bodySearch, createdAtGt, 
createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, 
dagIdPrefixPattern, dagRunId, limit, mapIndex, offset, orderBy, 
respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, 
taskId, taskIdPattern, taskIdPrefixPattern, teams }: {
   bodySearch?: string;
   createdAtGt?: string;
   createdAtGte?: string;
@@ -1505,7 +1506,8 @@ export const useTaskInstanceServiceGetHitlDetailsSuspense 
= <TData = Common.Task
   taskId?: string;
   taskIdPattern?: string;
   taskIdPrefixPattern?: string;
-}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: 
Common.UseTaskInstanceServiceGetHitlDetailsKeyFn({ bodySearch, createdAtGt, 
createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, 
dagIdPrefixPattern, dagRunId, limit, mapIndex, offset, orderBy, 
respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, 
taskId, taskIdPattern, taskIdPrefixPattern }, queryKey), queryFn:  [...]
+  teams?: string[];
+}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: 
Common.UseTaskInstanceServiceGetHitlDetailsKeyFn({ bodySearch, createdAtGt, 
createdAtGte, createdAtLt, createdAtLte, dagId, dagIdPattern, 
dagIdPrefixPattern, dagRunId, limit, mapIndex, offset, orderBy, 
respondedByUserId, respondedByUserName, responseReceived, state, subjectSearch, 
taskId, taskIdPattern, taskIdPrefixPattern, teams }, queryKey), qu [...]
 /**
 * Get Import Error
 * Get an import error.
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 86596b1b890..a5925040182 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
@@ -3506,6 +3506,7 @@ export class TaskInstanceService {
      * @param data.taskIdPrefixPattern Case-sensitive, index-friendly prefix 
match. See "Filtering with pattern parameters".
      * @param data.mapIndex
      * @param data.state
+     * @param data.teams
      * @param data.responseReceived
      * @param data.respondedByUserId
      * @param data.respondedByUserName
@@ -3537,6 +3538,7 @@ export class TaskInstanceService {
                 task_id_prefix_pattern: data.taskIdPrefixPattern,
                 map_index: data.mapIndex,
                 state: data.state,
+                teams: data.teams,
                 response_received: data.responseReceived,
                 responded_by_user_id: data.respondedByUserId,
                 responded_by_user_name: data.respondedByUserName,
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 6dcc310a469..189c9f182f7 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
@@ -4500,6 +4500,7 @@ export type GetHitlDetailsData = {
      * Case-sensitive, index-friendly prefix match. See "Filtering with 
pattern parameters".
      */
     taskIdPrefixPattern?: string | null;
+    teams?: Array<(string)>;
 };
 
 export type GetHitlDetailsResponse = HITLDetailCollection;
diff --git 
a/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLFilters.tsx 
b/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLFilters.tsx
index 9e1850c48e9..e9f66632b18 100644
--- a/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLFilters.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLFilters.tsx
@@ -21,10 +21,12 @@ import { useParams } from "react-router-dom";
 import { FilterBar } from "src/components/FilterBar";
 
 import { SearchParamsKeys } from "src/constants/searchParams";
+import { useConfig } from "src/queries/useConfig";
 import { useFiltersHandler, type FilterableSearchParamsKeys } from "src/utils";
 
 export const HITLFilters = ({ onResponseChange }: { readonly onResponseChange: 
() => void }) => {
   const { dagId = "~", taskId = "~" } = useParams();
+  const multiTeamEnabled = Boolean(useConfig("multi_team"));
 
   const fixedKeys: Array<FilterableSearchParamsKeys> = [
     SearchParamsKeys.RESPONSE_RECEIVED,
@@ -45,6 +47,10 @@ export const HITLFilters = ({ onResponseChange }: { readonly 
onResponseChange: (
     dynamicKeys.push(SearchParamsKeys.TASK_ID_PATTERN);
   }
 
+  if (multiTeamEnabled) {
+    dynamicKeys.push(SearchParamsKeys.TEAMS);
+  }
+
   const searchParamKeys = [...dynamicKeys, ...fixedKeys];
 
   const { filterConfigs, handleFiltersChange, initialValues } = 
useFiltersHandler(searchParamKeys);
diff --git 
a/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.test.tsx
 
b/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.test.tsx
index 766d2c8f1a8..1440662aa07 100644
--- 
a/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.test.tsx
+++ 
b/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.test.tsx
@@ -34,6 +34,10 @@ import { HITLTaskInstances } from "./HITLTaskInstances";
 // `useSearchParams` is replaced with a function that returns a mutable
 // reference; tests update `mockSearchParams` before each `render` call.
 let mockSearchParams = new URLSearchParams();
+const mockConfig: Record<string, unknown> = { multi_team: false };
+// Populated by the `DataTable` stub on every render, so the column set the 
page
+// builds can be asserted without rendering a real table.
+let renderedColumns: Array<{ accessorKey?: string }> = [];
 
 vi.mock("react-i18next", async (importOriginal) => {
   const actual = await importOriginal<typeof ReactI18Next>();
@@ -89,7 +93,15 @@ vi.mock("./HITLFilters", () => ({
 }));
 
 vi.mock("src/components/DataTable", () => ({
-  DataTable: () => null,
+  DataTable: ({ columns }: { columns: Array<{ accessorKey?: string }> }) => {
+    renderedColumns = columns;
+
+    return null;
+  },
+}));
+
+vi.mock("src/queries/useConfig", () => ({
+  useConfig: (key: string) => mockConfig[key],
 }));
 
 const { useTaskInstanceServiceGetHitlDetails } = await 
import("openapi/queries");
@@ -107,6 +119,8 @@ const lastListingCall = () => {
 };
 
 beforeEach(() => {
+  mockConfig.multi_team = false;
+  renderedColumns = [];
   vi.mocked(useTaskInstanceServiceGetHitlDetails).mockReturnValue(
     emptyHitlResponse as ReturnType<typeof 
useTaskInstanceServiceGetHitlDetails>,
   );
@@ -271,3 +285,49 @@ describe("HITLTaskInstances – auto-refresh predicate", () 
=> {
     expect(result).toBe(false);
   });
 });
+
+const columnKeys = () => renderedColumns.map((column) => column.accessorKey);
+
+describe("HITLTaskInstances – team column", () => {
+  it("shows the team right after the Dag it belongs to when multi-team is 
enabled", () => {
+    mockConfig.multi_team = true;
+    mockSearchParams = new URLSearchParams();
+
+    render(<HITLTaskInstances />, { wrapper: Wrapper });
+
+    const keys = columnKeys();
+
+    expect(keys).toContain("team_name");
+    
expect(keys.indexOf("team_name")).toBe(keys.indexOf("task_instance.dag_id") + 
1);
+  });
+
+  it("omits the team column when multi-team is disabled", () => {
+    mockSearchParams = new URLSearchParams();
+
+    render(<HITLTaskInstances />, { wrapper: Wrapper });
+
+    expect(columnKeys()).not.toContain("team_name");
+  });
+});
+
+describe("HITLTaskInstances – team filter", () => {
+  it("sends the selected teams when the teams URL param is set", () => {
+    mockSearchParams = new URLSearchParams("teams=team-a&teams=team-b");
+
+    render(<HITLTaskInstances />, { wrapper: Wrapper });
+
+    const args = lastListingCall()?.[0] as { teams?: Array<string> } | 
undefined;
+
+    expect(args?.teams).toEqual(["team-a", "team-b"]);
+  });
+
+  it("does not send a teams filter when the teams URL param is absent", () => {
+    mockSearchParams = new URLSearchParams();
+
+    render(<HITLTaskInstances />, { wrapper: Wrapper });
+
+    const args = lastListingCall()?.[0] as { teams?: Array<string> } | 
undefined;
+
+    expect(args?.teams).toBeUndefined();
+  });
+});
diff --git 
a/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.tsx 
b/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.tsx
index ce40f35981c..5d5536edb70 100644
--- 
a/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.tsx
+++ 
b/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.tsx
@@ -36,11 +36,13 @@ import { useTableURLState } from 
"src/components/DataTable/useTableUrlState";
 import { ErrorAlert } from "src/components/ErrorAlert";
 import { HITLReviewDrawer } from 
"src/components/HITLReview/HITLReviewDrawer.tsx";
 import { StateBadge } from "src/components/StateBadge";
+import { TeamName } from "src/components/TeamName";
 import Time from "src/components/Time";
 import { TruncatedText } from "src/components/TruncatedText";
 
 import { SearchParamsKeys, type SearchParamsKeysType } from 
"src/constants/searchParams";
 import { useAdvancedSearchArg } from "src/hooks/useAdvancedSearch";
+import { useConfig } from "src/queries/useConfig";
 import { useAutoRefresh, useDocumentTitle } from "src/utils";
 import { getHITLState, isHITLPending } from "src/utils/hitl";
 import { getTaskInstanceLink } from "src/utils/links";
@@ -60,6 +62,7 @@ const {
   RESPONSE_RECEIVED: RESPONSE_RECEIVED_PARAM,
   SUBJECT_SEARCH,
   TASK_ID_PATTERN,
+  TEAMS,
 }: SearchParamsKeysType = SearchParamsKeys;
 
 const HITLReviewDrawerButton = ({
@@ -99,12 +102,14 @@ const useHITLReviewDrawer = () => {
 
 const taskInstanceColumns = ({
   dagId,
+  multiTeam,
   renderHITLReviewDrawerButton,
   runId,
   taskId,
   translate,
 }: {
   dagId?: string;
+  multiTeam: boolean;
   renderHITLReviewDrawerButton?: (detail: HITLDetail) => ReactNode;
   runId?: string;
   taskId?: string;
@@ -149,6 +154,18 @@ const taskInstanceColumns = ({
           enableSorting: false,
           header: translate("common:dagId"),
         },
+        ...(multiTeam
+          ? [
+              {
+                accessorKey: "team_name",
+                cell: ({ row: { original } }: HITLRow) => (
+                  <TeamName teamName={original.task_instance.team_name} />
+                ),
+                enableSorting: false,
+                header: translate("common:dagDetails.team"),
+              },
+            ]
+          : []),
       ]),
   ...(Boolean(runId)
     ? []
@@ -226,6 +243,7 @@ export const HITLTaskInstances = ({
 }) => {
   const { t: translate } = useTranslation("hitl");
   const { dagId, runId, taskId } = useParams();
+  const multiTeamEnabled = Boolean(useConfig("multi_team"));
 
   // Only the standalone required-actions page owns the tab title; nested tabs 
inherit their parent's.
   useDocumentTitle(enableHITLReviewDrawer ? 
translate("common:browse.requiredActions") : undefined);
@@ -249,6 +267,7 @@ export const HITLTaskInstances = ({
   const filterResponseReceived = searchParams.get(RESPONSE_RECEIVED_PARAM) ?? 
undefined;
   const respondedByUserName = searchParams.get(RESPONDED_BY_USER_NAME) ?? 
undefined;
   const subjectSearch = searchParams.get(SUBJECT_SEARCH) ?? undefined;
+  const teams = searchParams.getAll(TEAMS);
 
   // Use the filter value if available, otherwise fall back to the old 
responseReceived param
   const effectiveResponseReceived = filterResponseReceived ?? responseReceived;
@@ -287,6 +306,7 @@ export const HITLTaskInstances = ({
       subjectSearch,
       taskId,
       ...taskIdArg,
+      teams: teams.length > 0 ? teams : undefined,
     },
     undefined,
     {
@@ -315,6 +335,7 @@ export const HITLTaskInstances = ({
 
   const columns = taskInstanceColumns({
     dagId,
+    multiTeam: multiTeamEnabled,
     renderHITLReviewDrawerButton: enableHITLReviewDrawer
       ? (detail) => <HITLReviewDrawerButton detail={detail} 
onOpen={openHITLReviewDrawer} />
       : undefined,
diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_hitl.py 
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_hitl.py
index 0df529b047a..75f7d65ecbd 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_hitl.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_hitl.py
@@ -25,18 +25,24 @@ from unittest import mock
 
 import pytest
 import time_machine
-from sqlalchemy import delete, select
+from sqlalchemy import delete, select, update
 from sqlalchemy.orm import Session
 
 from airflow._shared.serialization import CLASSNAME, FORBIDDEN_XCOM_KEYS
 from airflow._shared.timezones.timezone import utc, utcnow
+from airflow.models.dag import DagModel
+from airflow.models.dagbundle import DagBundleModel
 from airflow.models.hitl import HITLDetail
 from airflow.models.log import Log
 from airflow.models.taskinstance import TaskInstance as TIModel
+from airflow.models.team import Team
 from airflow.sdk.execution_time.hitl import HITLUser
+from airflow.utils.session import NEW_SESSION
 from airflow.utils.state import TaskInstanceState
 
 from tests_common.test_utils.asserts import assert_queries_count
+from tests_common.test_utils.config import conf_vars
+from tests_common.test_utils.db import clear_db_dag_bundles, clear_db_dags, 
clear_db_teams
 from tests_common.test_utils.format_datetime import 
from_datetime_to_zulu_without_ms
 
 if TYPE_CHECKING:
@@ -58,6 +64,16 @@ DEFAULT_CREATED_AT = datetime(2025, 9, 15, 13, 0, 0, 
tzinfo=utc)
 ANOTHER_CREATED_AT = datetime(2025, 9, 16, 12, 0, 0, tzinfo=utc)
 
 
+def _attach_dag_to_team(dag_id: str, team_name: str, *, session: Session = 
NEW_SESSION) -> None:
+    """Move a Dag into a team-scoped bundle, which is how a Dag gains a 
team."""
+    bundle = DagBundleModel(name=f"team-bundle-{team_name}")
+    bundle.teams.append(Team(name=team_name))
+    session.add(bundle)
+    session.flush()
+    session.execute(update(DagModel).where(DagModel.dag_id == 
dag_id).values(bundle_name=bundle.name))
+    session.commit()
+
+
 @pytest.fixture
 def sample_ti(
     create_task_instance: CreateTaskInstance,
@@ -774,6 +790,36 @@ class TestGetHITLDetailsEndpoint:
         assert response.json()["total_entries"] == expected_ti_count
         assert len(response.json()["hitl_details"]) == expected_ti_count
 
+    @conf_vars({("core", "multi_team"): "True"})
+    @pytest.mark.usefixtures("sample_hitl_details")
+    def test_should_respond_200_filtered_by_team(
+        self,
+        test_client: TestClient,
+        session: Session,
+    ) -> None:
+        _attach_dag_to_team("hitl_dag_0", "team-hitl", session=session)
+        try:
+            response = test_client.get("/dags/~/dagRuns/~/hitlDetails", 
params={"teams": ["team-hitl"]})
+            assert response.status_code == 200
+            response_data = response.json()
+            assert response_data["total_entries"] == 1
+            assert {detail["task_instance"]["dag_id"] for detail in 
response_data["hitl_details"]} == {
+                "hitl_dag_0"
+            }
+            assert {detail["task_instance"]["team_name"] for detail in 
response_data["hitl_details"]} == {
+                "team-hitl"
+            }
+
+            response = test_client.get(
+                "/dags/~/dagRuns/~/hitlDetails", params={"teams": 
["team-without-dags"]}
+            )
+            assert response.status_code == 200
+            assert response.json()["total_entries"] == 0
+        finally:
+            clear_db_dags()
+            clear_db_dag_bundles()
+            clear_db_teams()
+
     @pytest.mark.usefixtures("sample_hitl_details")
     def test_should_respond_200_with_existing_response_and_concrete_query(
         self,

Reply via email to