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 063d5f108da UI: Add team column and filter to Dag list page (#70028)
063d5f108da is described below

commit 063d5f108dad0ff0167814e65899aa7b95bd8f12
Author: Vincent <[email protected]>
AuthorDate: Tue Jul 21 15:32:47 2026 -0400

    UI: Add team column and filter to Dag list page (#70028)
    
    When multi-team mode is enabled, operators need to identify which team
    owns each Dag and filter the list by team. This adds:
    
    - A "Team" column (after tags) in the Dag list table that links to a
      filtered view of that team's Dags
    - A team multi-select filter in the header controls
    - Backend support: `team_name` field in the DAGWithLatestDagRunsResponse
      and a `teams` query parameter for filtering Dags by team (via bundle
      association)
    
    Both the column and filter are conditionally rendered only when the
    `multi_team` configuration is enabled.
---
 .../src/airflow/api_fastapi/common/parameters.py   | 25 +++++++
 .../api_fastapi/core_api/datamodels/ui/dags.py     |  1 +
 .../api_fastapi/core_api/openapi/_private_ui.yaml  | 13 ++++
 .../airflow/api_fastapi/core_api/routes/ui/dags.py | 11 +++
 .../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 +-
 .../airflow/ui/openapi-gen/requests/schemas.gen.ts | 11 +++
 .../ui/openapi-gen/requests/services.gen.ts        |  2 +
 .../airflow/ui/openapi-gen/requests/types.gen.ts   |  2 +
 .../airflow/ui/public/i18n/locales/en/common.json  |  4 +-
 .../src/airflow/ui/src/constants/searchParams.ts   |  1 +
 .../src/airflow/ui/src/pages/DagsList/DagCard.tsx  | 23 +++++-
 .../src/pages/DagsList/DagsFilters/DagsFilters.tsx | 16 +++++
 .../src/pages/DagsList/DagsFilters/TeamFilter.tsx  | 81 ++++++++++++++++++++++
 .../src/airflow/ui/src/pages/DagsList/DagsList.tsx | 24 ++++++-
 .../src/airflow/ui/src/queries/useDags.tsx         |  3 +
 19 files changed, 232 insertions(+), 14 deletions(-)

diff --git a/airflow-core/src/airflow/api_fastapi/common/parameters.py 
b/airflow-core/src/airflow/api_fastapi/common/parameters.py
index 31471d9e26a..e4c7b1bfd5d 100644
--- a/airflow-core/src/airflow/api_fastapi/common/parameters.py
+++ b/airflow-core/src/airflow/api_fastapi/common/parameters.py
@@ -62,6 +62,7 @@ from airflow.models.connection import Connection
 from airflow.models.dag import DagModel, DagTag
 from airflow.models.dag_favorite import DagFavorite
 from airflow.models.dag_version import DagVersion
+from airflow.models.dagbundle import DagBundleModel
 from airflow.models.dagrun import DagRun
 from airflow.models.errors import ParseImportError
 from airflow.models.hitl import HITLDetail
@@ -1020,6 +1021,29 @@ class _OwnersFilter(BaseParam[list[str]]):
         return cls().set_value(owners)
 
 
+class _TeamsFilter(BaseParam[list[str]]):
+    """Filter Dags by team name (via bundle association)."""
+
+    def to_orm(self, select: Select) -> Select:
+        if self.skip_none is False:
+            raise ValueError(f"Cannot set 'skip_none' to False on a 
{type(self)}")
+
+        if not self.value:
+            return select
+
+        from airflow.models.team import Team
+
+        return select.where(
+            DagModel.bundle_name.in_(
+                
sql_select(DagBundleModel.name).join(DagBundleModel.teams).where(Team.name.in_(self.value))
+            )
+        )
+
+    @classmethod
+    def depends(cls, teams: list[str] = Query(default_factory=list)) -> 
_TeamsFilter:
+        return cls().set_value(teams)
+
+
 def _safe_parse_datetime(date_to_check: str) -> datetime:
     """
     Parse datetime and raise error for invalid dates.
@@ -1268,6 +1292,7 @@ QueryDagIdPrefixPatternSearchWithNone = Annotated[
 ]
 QueryTagsFilter = Annotated[_TagsFilter, Depends(_TagsFilter.depends)]
 QueryOwnersFilter = Annotated[_OwnersFilter, Depends(_OwnersFilter.depends)]
+QueryTeamsFilter = Annotated[_TeamsFilter, Depends(_TeamsFilter.depends)]
 
 
 class _HasAssetScheduleFilter(BaseParam[bool]):
diff --git 
a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/dags.py 
b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/dags.py
index 590084cc2c5..e49e89087da 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/dags.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/dags.py
@@ -32,6 +32,7 @@ class DAGWithLatestDagRunsResponse(DAGResponse):
     latest_dag_runs: list[DAGRunLightResponse]
     pending_actions: list[HITLDetail]
     is_favorite: bool
+    team_name: str | None = None
 
 
 class DAGWithLatestDagRunsCollectionResponse(BaseModel):
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 a0b1b1b9821..e34789c3524 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
@@ -338,6 +338,14 @@ paths:
           items:
             type: string
           title: Owners
+      - name: teams
+        in: query
+        required: false
+        schema:
+          type: array
+          items:
+            type: string
+          title: Teams
       - name: dag_ids
         in: query
         required: false
@@ -2629,6 +2637,11 @@ components:
         is_favorite:
           type: boolean
           title: Is Favorite
+        team_name:
+          anyOf:
+          - type: string
+          - type: 'null'
+          title: Team Name
         is_backfillable:
           type: boolean
           title: Is Backfillable
diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dags.py 
b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dags.py
index 8e2d185d1c5..38421d387b1 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dags.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dags.py
@@ -51,6 +51,7 @@ from airflow.api_fastapi.common.parameters import (
     QueryPausedFilter,
     QueryPendingActionsFilter,
     QueryTagsFilter,
+    QueryTeamsFilter,
     SortParam,
     filter_param_factory,
 )
@@ -96,6 +97,7 @@ def get_dags(
     offset: QueryOffset,
     tags: QueryTagsFilter,
     owners: QueryOwnersFilter,
+    teams: QueryTeamsFilter,
     dag_ids: Annotated[
         FilterParam[list[str] | None],
         Depends(filter_param_factory(DagModel.dag_id, list[str] | None, 
FilterOptionEnum.IN, "dag_ids")),
@@ -157,6 +159,7 @@ def get_dags(
             dag_display_name_prefix_pattern,
             tags,
             owners,
+            teams,
             last_dag_run_state,
             dag_run_state,
             is_favorite,
@@ -232,6 +235,13 @@ def get_dags(
         for dag_id, hitl_detail in pending_actions:
             pending_actions_by_dag_id[dag_id].append(hitl_detail)
 
+    # Fetch team names when multi-team is enabled
+    team_names_by_dag_id: dict[str, str | None] = {}
+    if conf.getboolean("core", "multi_team") and dags:
+        team_names_by_dag_id = DagModel.get_dag_id_to_team_name_mapping(
+            [dag.dag_id for dag in dags], session=session
+        )
+
     # aggregate rows by dag_id
     # Build the dict dynamically from DAGResponse.model_fields so that new 
fields
     # added to DAGResponse are picked up automatically without code changes 
here.
@@ -249,6 +259,7 @@ def get_dags(
                 "latest_dag_runs": [],
                 "pending_actions": pending_actions_by_dag_id[dag.dag_id],
                 "is_favorite": dag.dag_id in favorite_dag_ids,
+                "team_name": team_names_by_dag_id.get(dag.dag_id),
             }
         )
         dag_runs_by_dag_id[dag.dag_id] = 
DAGWithLatestDagRunsResponse.model_validate(dag_data)
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 286a724232e..1cf5105afed 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
@@ -339,7 +339,7 @@ export const UseDagServiceGetDagTagsKeyFn = ({ limit, 
offset, orderBy, tagNamePa
 export type DagServiceGetDagsUiDefaultResponse = Awaited<ReturnType<typeof 
DagService.getDagsUi>>;
 export type DagServiceGetDagsUiQueryResult<TData = 
DagServiceGetDagsUiDefaultResponse, TError = unknown> = UseQueryResult<TData, 
TError>;
 export const useDagServiceGetDagsUiKey = "DagServiceGetDagsUi";
-export const UseDagServiceGetDagsUiKeyFn = ({ assetDependency, bundleName, 
bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, 
dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, 
excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, 
lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode }: 
{
+export const UseDagServiceGetDagsUiKeyFn = ({ assetDependency, bundleName, 
bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, 
dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, 
excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, 
lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, 
teams }: {
   assetDependency?: string;
   bundleName?: string;
   bundleVersion?: string;
@@ -363,7 +363,8 @@ export const UseDagServiceGetDagsUiKeyFn = ({ 
assetDependency, bundleName, bundl
   paused?: boolean;
   tags?: string[];
   tagsMatchMode?: "any" | "all";
-} = {}, queryKey?: Array<unknown>) => [useDagServiceGetDagsUiKey, ...(queryKey 
?? [{ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, 
dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, 
dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, 
hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, 
paused, tags, tagsMatchMode }])];
+  teams?: string[];
+} = {}, queryKey?: Array<unknown>) => [useDagServiceGetDagsUiKey, ...(queryKey 
?? [{ assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, 
dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, 
dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, 
hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, 
paused, tags, tagsMatchMode, teams }])];
 export type DagServiceGetLatestRunInfoDefaultResponse = 
Awaited<ReturnType<typeof DagService.getLatestRunInfo>>;
 export type DagServiceGetLatestRunInfoQueryResult<TData = 
DagServiceGetLatestRunInfoDefaultResponse, TError = unknown> = 
UseQueryResult<TData, TError>;
 export const useDagServiceGetLatestRunInfoKey = "DagServiceGetLatestRunInfo";
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 15dceaa74b9..8fbebba6f50 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
@@ -675,6 +675,7 @@ export const ensureUseDagServiceGetDagTagsData = 
(queryClient: QueryClient, { li
 * @param data.tags
 * @param data.tagsMatchMode
 * @param data.owners
+* @param data.teams
 * @param data.dagIds
 * @param data.dagIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. 
`%customer_%`). Use the pipe `|` operator for OR logic (e.g. `dag1 | dag2`). 
Regular expressions are **not** supported.
 *
@@ -699,7 +700,7 @@ export const ensureUseDagServiceGetDagTagsData = 
(queryClient: QueryClient, { li
 * @returns DAGWithLatestDagRunsCollectionResponse Successful Response
 * @throws ApiError
 */
-export const ensureUseDagServiceGetDagsUiData = (queryClient: QueryClient, { 
assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, 
dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, 
dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, 
hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, 
paused, tags, tagsMatchMode }: {
+export const ensureUseDagServiceGetDagsUiData = (queryClient: QueryClient, { 
assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, 
dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, 
dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, 
hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, 
paused, tags, tagsMatchMode, teams }: {
   assetDependency?: string;
   bundleName?: string;
   bundleVersion?: string;
@@ -723,7 +724,8 @@ export const ensureUseDagServiceGetDagsUiData = 
(queryClient: QueryClient, { ass
   paused?: boolean;
   tags?: string[];
   tagsMatchMode?: "any" | "all";
-} = {}) => queryClient.ensureQueryData({ queryKey: 
Common.UseDagServiceGetDagsUiKeyFn({ assetDependency, bundleName, 
bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, 
dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, 
excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, 
lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode 
}), queryFn: () => DagService.getDagsUi({ assetDependency, bundleName, 
bundleVersi [...]
+  teams?: string[];
+} = {}) => queryClient.ensureQueryData({ queryKey: 
Common.UseDagServiceGetDagsUiKeyFn({ assetDependency, bundleName, 
bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, 
dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, 
excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, 
lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, 
teams }), queryFn: () => DagService.getDagsUi({ assetDependency, bundleName, 
bund [...]
 /**
 * Get Latest Run Info
 * Get latest run.
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts 
b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
index 822dde5ad39..e6937375f25 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
@@ -675,6 +675,7 @@ export const prefetchUseDagServiceGetDagTags = 
(queryClient: QueryClient, { limi
 * @param data.tags
 * @param data.tagsMatchMode
 * @param data.owners
+* @param data.teams
 * @param data.dagIds
 * @param data.dagIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. 
`%customer_%`). Use the pipe `|` operator for OR logic (e.g. `dag1 | dag2`). 
Regular expressions are **not** supported.
 *
@@ -699,7 +700,7 @@ export const prefetchUseDagServiceGetDagTags = 
(queryClient: QueryClient, { limi
 * @returns DAGWithLatestDagRunsCollectionResponse Successful Response
 * @throws ApiError
 */
-export const prefetchUseDagServiceGetDagsUi = (queryClient: QueryClient, { 
assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, 
dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, 
dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, 
hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, 
paused, tags, tagsMatchMode }: {
+export const prefetchUseDagServiceGetDagsUi = (queryClient: QueryClient, { 
assetDependency, bundleName, bundleVersion, dagDisplayNamePattern, 
dagDisplayNamePrefixPattern, dagIdPattern, dagIdPrefixPattern, dagIds, 
dagRunsLimit, dagRunState, excludeStale, hasAssetSchedule, hasImportErrors, 
hasPendingActions, isFavorite, lastDagRunState, limit, offset, orderBy, owners, 
paused, tags, tagsMatchMode, teams }: {
   assetDependency?: string;
   bundleName?: string;
   bundleVersion?: string;
@@ -723,7 +724,8 @@ export const prefetchUseDagServiceGetDagsUi = (queryClient: 
QueryClient, { asset
   paused?: boolean;
   tags?: string[];
   tagsMatchMode?: "any" | "all";
-} = {}) => queryClient.prefetchQuery({ queryKey: 
Common.UseDagServiceGetDagsUiKeyFn({ assetDependency, bundleName, 
bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, 
dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, 
excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, 
lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode 
}), queryFn: () => DagService.getDagsUi({ assetDependency, bundleName, 
bundleVersion [...]
+  teams?: string[];
+} = {}) => queryClient.prefetchQuery({ queryKey: 
Common.UseDagServiceGetDagsUiKeyFn({ assetDependency, bundleName, 
bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, 
dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, 
excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, 
lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, 
teams }), queryFn: () => DagService.getDagsUi({ assetDependency, bundleName, 
bundle [...]
 /**
 * Get Latest Run Info
 * Get latest run.
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts 
b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
index 74cbc95ff57..c586eabf179 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
@@ -675,6 +675,7 @@ export const useDagServiceGetDagTags = <TData = 
Common.DagServiceGetDagTagsDefau
 * @param data.tags
 * @param data.tagsMatchMode
 * @param data.owners
+* @param data.teams
 * @param data.dagIds
 * @param data.dagIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. 
`%customer_%`). Use the pipe `|` operator for OR logic (e.g. `dag1 | dag2`). 
Regular expressions are **not** supported.
 *
@@ -699,7 +700,7 @@ export const useDagServiceGetDagTags = <TData = 
Common.DagServiceGetDagTagsDefau
 * @returns DAGWithLatestDagRunsCollectionResponse Successful Response
 * @throws ApiError
 */
-export const useDagServiceGetDagsUi = <TData = 
Common.DagServiceGetDagsUiDefaultResponse, TError = unknown, TQueryKey extends 
Array<unknown> = unknown[]>({ assetDependency, bundleName, bundleVersion, 
dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, 
dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, 
hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, 
lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode }: 
{
+export const useDagServiceGetDagsUi = <TData = 
Common.DagServiceGetDagsUiDefaultResponse, TError = unknown, TQueryKey extends 
Array<unknown> = unknown[]>({ assetDependency, bundleName, bundleVersion, 
dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, 
dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, 
hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, 
lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, 
teams }: {
   assetDependency?: string;
   bundleName?: string;
   bundleVersion?: string;
@@ -723,7 +724,8 @@ export const useDagServiceGetDagsUi = <TData = 
Common.DagServiceGetDagsUiDefault
   paused?: boolean;
   tags?: string[];
   tagsMatchMode?: "any" | "all";
-} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: 
Common.UseDagServiceGetDagsUiKeyFn({ assetDependency, bundleName, 
bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, 
dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, 
excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, 
lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMa [...]
+  teams?: string[];
+} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey: 
Common.UseDagServiceGetDagsUiKeyFn({ assetDependency, bundleName, 
bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, 
dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, 
excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, 
lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMa [...]
 /**
 * Get Latest Run Info
 * Get latest run.
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts 
b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
index 962719f88aa..3bc869e2b7d 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
@@ -675,6 +675,7 @@ export const useDagServiceGetDagTagsSuspense = <TData = 
Common.DagServiceGetDagT
 * @param data.tags
 * @param data.tagsMatchMode
 * @param data.owners
+* @param data.teams
 * @param data.dagIds
 * @param data.dagIdPattern SQL LIKE expression — use `%` / `_` wildcards (e.g. 
`%customer_%`). Use the pipe `|` operator for OR logic (e.g. `dag1 | dag2`). 
Regular expressions are **not** supported.
 *
@@ -699,7 +700,7 @@ export const useDagServiceGetDagTagsSuspense = <TData = 
Common.DagServiceGetDagT
 * @returns DAGWithLatestDagRunsCollectionResponse Successful Response
 * @throws ApiError
 */
-export const useDagServiceGetDagsUiSuspense = <TData = 
Common.DagServiceGetDagsUiDefaultResponse, TError = unknown, TQueryKey extends 
Array<unknown> = unknown[]>({ assetDependency, bundleName, bundleVersion, 
dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, 
dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, 
hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, 
lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode }: 
{
+export const useDagServiceGetDagsUiSuspense = <TData = 
Common.DagServiceGetDagsUiDefaultResponse, TError = unknown, TQueryKey extends 
Array<unknown> = unknown[]>({ assetDependency, bundleName, bundleVersion, 
dagDisplayNamePattern, dagDisplayNamePrefixPattern, dagIdPattern, 
dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, excludeStale, 
hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, 
lastDagRunState, limit, offset, orderBy, owners, paused, tags, tagsMatchMode, 
teams }: {
   assetDependency?: string;
   bundleName?: string;
   bundleVersion?: string;
@@ -723,7 +724,8 @@ export const useDagServiceGetDagsUiSuspense = <TData = 
Common.DagServiceGetDagsU
   paused?: boolean;
   tags?: string[];
   tagsMatchMode?: "any" | "all";
-} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: 
Common.UseDagServiceGetDagsUiKeyFn({ assetDependency, bundleName, 
bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, 
dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, 
excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, 
lastDagRunState, limit, offset, orderBy, owners, paused, tags [...]
+  teams?: string[];
+} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>, 
"queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey: 
Common.UseDagServiceGetDagsUiKeyFn({ assetDependency, bundleName, 
bundleVersion, dagDisplayNamePattern, dagDisplayNamePrefixPattern, 
dagIdPattern, dagIdPrefixPattern, dagIds, dagRunsLimit, dagRunState, 
excludeStale, hasAssetSchedule, hasImportErrors, hasPendingActions, isFavorite, 
lastDagRunState, limit, offset, orderBy, owners, paused, tags [...]
 /**
 * Get Latest Run Info
 * Get latest run.
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts 
b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
index e279a291f8b..29287fe53d2 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
@@ -9431,6 +9431,17 @@ export const $DAGWithLatestDagRunsResponse = {
             type: 'boolean',
             title: 'Is Favorite'
         },
+        team_name: {
+            anyOf: [
+                {
+                    type: 'string'
+                },
+                {
+                    type: 'null'
+                }
+            ],
+            title: 'Team Name'
+        },
         is_backfillable: {
             type: 'boolean',
             title: 'Is Backfillable',
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 dd68e8cc94b..3ae4565069c 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
@@ -1950,6 +1950,7 @@ export class DagService {
      * @param data.tags
      * @param data.tagsMatchMode
      * @param data.owners
+     * @param data.teams
      * @param data.dagIds
      * @param data.dagIdPattern SQL LIKE expression — use `%` / `_` wildcards 
(e.g. `%customer_%`). Use the pipe `|` operator for OR logic (e.g. `dag1 | 
dag2`). Regular expressions are **not** supported.
      *
@@ -1985,6 +1986,7 @@ export class DagService {
                 tags: data.tags,
                 tags_match_mode: data.tagsMatchMode,
                 owners: data.owners,
+                teams: data.teams,
                 dag_ids: data.dagIds,
                 dag_id_pattern: data.dagIdPattern,
                 dag_id_prefix_pattern: data.dagIdPrefixPattern,
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 7464f43b2cd..d16375c10a8 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
@@ -2378,6 +2378,7 @@ export type DAGWithLatestDagRunsResponse = {
     latest_dag_runs: Array<DAGRunLightResponse>;
     pending_actions: Array<HITLDetail>;
     is_favorite: boolean;
+    team_name?: string | null;
     /**
      * Whether this Dag's schedule supports backfilling.
      */
@@ -3566,6 +3567,7 @@ export type GetDagsUiData = {
     paused?: boolean | null;
     tags?: Array<(string)>;
     tagsMatchMode?: 'any' | 'all' | null;
+    teams?: Array<(string)>;
 };
 
 export type GetDagsUiResponse = DAGWithLatestDagRunsCollectionResponse;
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 e27deafb42b..9beb02245b6 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
@@ -57,7 +57,8 @@
     "owner": "Owner",
     "params": "Params",
     "schedule": "Schedule",
-    "tags": "Tags"
+    "tags": "Tags",
+    "team": "Team"
   },
   "dagId": "Dag ID",
   "dagRun": {
@@ -324,6 +325,7 @@
     "from": "From",
     "maxActiveRuns": "Max Active Runs",
     "noTagsFound": "No tags found",
+    "noTeamsFound": "No teams found",
     "tagMode": {
       "all": "All",
       "any": "Any"
diff --git a/airflow-core/src/airflow/ui/src/constants/searchParams.ts 
b/airflow-core/src/airflow/ui/src/constants/searchParams.ts
index b6f4d88e3d0..eee952f0207 100644
--- a/airflow-core/src/airflow/ui/src/constants/searchParams.ts
+++ b/airflow-core/src/airflow/ui/src/constants/searchParams.ts
@@ -104,6 +104,7 @@ export enum SearchParamsKeys {
   TASK_ID = "task_id",
   TASK_ID_PATTERN = "task_id_pattern",
   TASK_STATE = "task_state",
+  TEAMS = "teams",
   TRIGGER_RULE = "trigger_rule",
   TRIGGERING_USER = "triggering_user",
   TRIGGERING_USER_NAME_PATTERN = "triggering_user_name_pattern",
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx
index cd320d417d0..1d77fb6660a 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx
@@ -28,6 +28,7 @@ import { Stat } from "src/components/Stat";
 import { TogglePause } from "src/components/TogglePause";
 import { TriggerDAGButton } from "src/components/TriggerDag/TriggerDAGButton";
 import { RouterLink, Tooltip } from "src/components/ui";
+import { useConfig } from "src/queries/useConfig";
 import { isStatePending, useAutoRefresh } from "src/utils";
 
 import { DagRunStateCounts } from "./DagRunStateCounts";
@@ -45,6 +46,7 @@ type Props = {
 export const DagCard = ({ dag, runStateCounts, runStateCountsLoading, 
stateCountLimit }: Props) => {
   const { t: translate } = useTranslation(["common", "dag"]);
   const [latestRun] = dag.latest_dag_runs;
+  const multiTeamEnabled = Boolean(useConfig("multi_team"));
 
   const refetchInterval = useAutoRefresh({});
 
@@ -78,7 +80,13 @@ export const DagCard = ({ dag, runStateCounts, 
runStateCountsLoading, stateCount
           <DeleteDagButton dagDisplayName={dag.dag_display_name} 
dagId={dag.dag_id} />
         </HStack>
       </Flex>
-      <Grid gap={1} px={3} py={2} templateColumns="repeat(4, 1fr)" 
templateRows="auto auto">
+      <Grid
+        gap={1}
+        px={3}
+        py={2}
+        templateColumns={multiTeamEnabled ? "repeat(5, 1fr)" : "repeat(4, 
1fr)"}
+        templateRows="auto auto"
+      >
         <GridItem gridColumn={1} gridRow={1}>
           <Stat data-testid="schedule" 
label={translate("dagDetails.schedule")}>
             <Schedule
@@ -118,10 +126,21 @@ export const DagCard = ({ dag, runStateCounts, 
runStateCountsLoading, stateCount
             ) : undefined}
           </Stat>
         </GridItem>
+        {multiTeamEnabled ? (
+          <GridItem gridColumn={4} gridRow={1}>
+            <Stat label={translate("dagDetails.team")}>
+              {dag.team_name === undefined || dag.team_name === null ? 
undefined : (
+                <RouterLink 
to={`/dags?teams=${encodeURIComponent(dag.team_name)}`}>
+                  {dag.team_name}
+                </RouterLink>
+              )}
+            </Stat>
+          </GridItem>
+        ) : undefined}
         <GridItem
           alignItems="flex-end"
           display="flex"
-          gridColumn={4}
+          gridColumn={multiTeamEnabled ? 5 : 4}
           gridRow="1 / 3"
           justifyContent="flex-end"
         >
diff --git 
a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx
index 9f1863bc120..74300bf28a3 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx
@@ -33,6 +33,7 @@ import { PausedFilter } from "./PausedFilter";
 import { RequiredActionFilter } from "./RequiredActionFilter";
 import { RunStateSelect } from "./RunStateSelect";
 import { TagFilter } from "./TagFilter";
+import { TeamFilter } from "./TeamFilter";
 
 const {
   DAG_RUN_STATE: DAG_RUN_STATE_PARAM,
@@ -41,6 +42,7 @@ const {
   NEEDS_REVIEW: NEEDS_REVIEW_PARAM,
   OFFSET: OFFSET_PARAM,
   PAUSED: PAUSED_PARAM,
+  TEAMS: TEAMS_PARAM,
 }: SearchParamsKeysType = SearchParamsKeys;
 
 type BooleanFilterValue = "all" | "false" | "true";
@@ -60,12 +62,14 @@ export const DagsFilters = () => {
   const [searchParams, setSearchParams] = useSearchParams();
   const { t: translate } = useTranslation("dags");
   const { selectedTags, setSelectedTags, setTagFilterMode, tagFilterMode } = 
useTagFilter();
+  const multiTeamEnabled = Boolean(useConfig("multi_team"));
 
   const showPaused = searchParams.get(PAUSED_PARAM);
   const showFavorites = searchParams.get(FAVORITE_PARAM);
   const needsReview = searchParams.get(NEEDS_REVIEW_PARAM);
   const state = searchParams.get(LAST_DAG_RUN_STATE_PARAM);
   const activeRunState = searchParams.get(DAG_RUN_STATE_PARAM);
+  const selectedTeams = searchParams.getAll(TEAMS_PARAM);
 
   const [pattern, setPattern] = useState("");
 
@@ -152,6 +156,15 @@ export const DagsFilters = () => {
     setTagFilterMode(checked ? "all" : "any");
   };
 
+  const handleTeamsChange = (teams: Array<string>) => {
+    searchParams.delete(TEAMS_PARAM);
+    for (const team of teams) {
+      searchParams.append(TEAMS_PARAM, team);
+    }
+    resetPagination();
+    setSearchParams(searchParams);
+  };
+
   const pausedValue = toBooleanFilterValue(showPaused, defaultShowPaused);
   const favoriteValue = toBooleanFilterValue(showFavorites);
 
@@ -188,6 +201,9 @@ export const DagsFilters = () => {
         tags={data?.pages.flatMap((dagResponse) => dagResponse.tags) ?? []}
       />
       <FavoriteFilter onChange={handleFavoriteChange} value={favoriteValue} />
+      {multiTeamEnabled ? (
+        <TeamFilter onChange={handleTeamsChange} selectedTeams={selectedTeams} 
/>
+      ) : undefined}
     </HStack>
   );
 };
diff --git 
a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TeamFilter.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TeamFilter.tsx
new file mode 100644
index 00000000000..3b315aa8317
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TeamFilter.tsx
@@ -0,0 +1,81 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { Box, Field } from "@chakra-ui/react";
+import { Select as ReactSelect, type MultiValue } from "chakra-react-select";
+import { useTranslation } from "react-i18next";
+
+import { useTeamsServiceListTeams } from "openapi/queries";
+
+type Props = {
+  readonly onChange: (teams: Array<string>) => void;
+  readonly selectedTeams: Array<string>;
+};
+
+export const TeamFilter = ({ onChange, selectedTeams }: Props) => {
+  const { t: translate } = useTranslation("common");
+  const { data } = useTeamsServiceListTeams({ orderBy: ["name"] });
+
+  const options = (data?.teams ?? []).map((team) => ({
+    label: team.name,
+    value: team.name,
+  }));
+
+  const handleChange = (selected: MultiValue<{ label: string; value: string 
}>) => {
+    onChange(selected.map(({ value }) => value));
+  };
+
+  return (
+    <Box>
+      <Field.Root>
+        <ReactSelect
+          aria-label={translate("dagDetails.team")}
+          chakraStyles={{
+            clearIndicator: (provided) => ({
+              ...provided,
+              color: "gray.fg",
+            }),
+            container: (provided) => ({
+              ...provided,
+              maxWidth: 200,
+              minWidth: 64,
+            }),
+            control: (provided) => ({
+              ...provided,
+              colorPalette: "brand",
+            }),
+            menu: (provided) => ({
+              ...provided,
+              zIndex: 2,
+            }),
+          }}
+          isClearable
+          isMulti
+          noOptionsMessage={() => translate("table.noTeamsFound")}
+          onChange={handleChange}
+          options={options}
+          placeholder={translate("dagDetails.team")}
+          value={selectedTeams.map((team) => ({
+            label: team,
+            value: team,
+          }))}
+        />
+      </Field.Root>
+    </Box>
+  );
+};
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
index 56e8117a96d..f82a9e90c42 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
@@ -53,6 +53,10 @@ import { Schedule } from "./Schedule";
 import { SortSelect } from "./SortSelect";
 import { useTagFilter } from "./useTagFilter";
 
+type GetColumnsParams = {
+  readonly multiTeam: boolean;
+};
+
 type RunStateCountsContext = {
   readonly countsByDag: Record<string, Record<string, number> | undefined>;
   readonly isLoading: boolean;
@@ -62,6 +66,7 @@ type RunStateCountsContext = {
 const createColumns = (
   translate: (key: string, options?: Record<string, unknown>) => string,
   runStateContext: RunStateCountsContext,
+  { multiTeam }: GetColumnsParams,
 ): Array<ColumnDef<DAGWithLatestDagRunsResponse>> => [
   {
     accessorKey: "is_paused",
@@ -155,6 +160,19 @@ const createColumns = (
     enableSorting: false,
     header: () => translate("dagDetails.tags"),
   },
+  ...(multiTeam
+    ? [
+        {
+          accessorKey: "team_name",
+          cell: ({ row: { original } }: { row: { original: 
DAGWithLatestDagRunsResponse } }) =>
+            original.team_name !== undefined && original.team_name !== null ? (
+              <RouterLink 
to={`/dags?teams=${original.team_name}`}>{original.team_name}</RouterLink>
+            ) : undefined,
+          enableSorting: false,
+          header: () => translate("dagDetails.team"),
+        },
+      ]
+    : []),
   {
     accessorKey: "pending_actions",
     cell: ({ row: { original: dag } }) => <NeedsReviewBadge 
pendingActions={dag.pending_actions} />,
@@ -202,6 +220,7 @@ const {
   OFFSET,
   OWNERS,
   PAUSED,
+  TEAMS,
 }: SearchParamsKeysType = SearchParamsKeys;
 
 const createCardDef = (runStateContext: RunStateCountsContext): 
CardDef<DAGWithLatestDagRunsResponse> => ({
@@ -228,6 +247,7 @@ export const DagsList = () => {
   const dagRunsLimit = display === "card" ? 14 : 1;
 
   const hidePausedDagsByDefault = 
Boolean(useConfig("hide_paused_dags_by_default"));
+  const multiTeamEnabled = Boolean(useConfig("multi_team"));
   const defaultShowPaused = hidePausedDagsByDefault ? false : undefined;
 
   const showPaused = searchParams.get(PAUSED);
@@ -238,6 +258,7 @@ export const DagsList = () => {
   const { selectedTags, tagFilterMode: selectedMatchMode } = useTagFilter();
   const pendingReviews = searchParams.get(NEEDS_REVIEW);
   const owners = searchParams.getAll(OWNERS);
+  const teams = searchParams.getAll(TEAMS);
 
   const { setTableURLState, tableURLState } = useTableURLState();
 
@@ -301,6 +322,7 @@ export const DagsList = () => {
     pendingHitl,
     tags: selectedTags,
     tagsMatchMode: selectedMatchMode,
+    teams: teams.length > 0 ? teams : undefined,
   });
 
   const { data: runStateCountsData, isLoading: runStateCountsLoading } = 
useDagRunStateCounts({
@@ -315,7 +337,7 @@ export const DagsList = () => {
     stateCountLimit: runStateCountsData?.state_count_limit,
   };
 
-  const columns = createColumns(translate, runStateContext);
+  const columns = createColumns(translate, runStateContext, { multiTeam: 
multiTeamEnabled });
   const cardDef = createCardDef(runStateContext);
 
   const handleSortChange = ({ value }: 
SelectValueChangeDetails<Array<string>>) => {
diff --git a/airflow-core/src/airflow/ui/src/queries/useDags.tsx 
b/airflow-core/src/airflow/ui/src/queries/useDags.tsx
index 3517a7386e3..43d2cab5769 100644
--- a/airflow-core/src/airflow/ui/src/queries/useDags.tsx
+++ b/airflow-core/src/airflow/ui/src/queries/useDags.tsx
@@ -37,6 +37,7 @@ export const useDags = ({
   pendingHitl,
   tags,
   tagsMatchMode,
+  teams,
 }: {
   advancedSearch?: boolean;
   dagDisplayNamePattern?: string;
@@ -54,6 +55,7 @@ export const useDags = ({
   pendingHitl?: boolean;
   tags?: Array<string>;
   tagsMatchMode?: "all" | "any";
+  teams?: Array<string>;
 }) => {
   const refetchInterval = useAutoRefresh({});
 
@@ -75,6 +77,7 @@ export const useDags = ({
       paused,
       tags,
       tagsMatchMode,
+      teams,
     },
     undefined,
     {


Reply via email to