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,
{