This is an automated email from the ASF dual-hosted git repository.
bbovenzi 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 e22c9d02b2e Add Has Events filter to Assets search (#72460)
e22c9d02b2e is described below
commit e22c9d02b2e876dc20b69af1cfa0573114e3cc31
Author: jasperjonkhans <[email protected]>
AuthorDate: Wed Sep 30 21:47:08 2026 +0200
Add Has Events filter to Assets search (#72460)
* Add Has Events filter to Assets search
Large asset catalogs expose event activity only row by row, making it
difficult to distinguish live assets from definitions that have never
been updated.
* Document the Assets Has Events filter
* Polish Assets search filter labels and parameter handling
* Omit release note for the Assets search filter
The reviewer requested omitting a newsfragment for this change.
---
.../api_fastapi/common/parameters/__init__.py | 1 +
.../airflow/api_fastapi/common/parameters/asset.py | 23 ++++++++++++++++++
.../api_fastapi/core_api/openapi/_private_ui.yaml | 10 ++++++++
.../api_fastapi/core_api/routes/ui/assets.py | 3 +++
.../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 | 4 ++++
.../airflow/ui/public/i18n/locales/en/assets.json | 1 +
.../airflow/ui/public/i18n/locales/en/common.json | 4 +++-
.../src/airflow/ui/src/constants/filterConfigs.tsx | 9 +++++++
.../src/airflow/ui/src/constants/searchParams.ts | 1 +
.../ui/src/pages/AssetsList/AssetsList.test.tsx | 28 ++++++++++++++++++++++
.../airflow/ui/src/pages/AssetsList/AssetsList.tsx | 11 +++++++++
.../src/airflow/ui/src/utils/useFiltersHandler.ts | 1 +
.../api_fastapi/core_api/routes/ui/test_assets.py | 23 ++++++++++++++++++
19 files changed, 139 insertions(+), 11 deletions(-)
diff --git a/airflow-core/src/airflow/api_fastapi/common/parameters/__init__.py
b/airflow-core/src/airflow/api_fastapi/common/parameters/__init__.py
index a3fca665e9a..fa1a24866b0 100644
--- a/airflow-core/src/airflow/api_fastapi/common/parameters/__init__.py
+++ b/airflow-core/src/airflow/api_fastapi/common/parameters/__init__.py
@@ -33,6 +33,7 @@ from airflow.api_fastapi.common.parameters.asset import (
QueryAssetNamePrefixPatternSearch as QueryAssetNamePrefixPatternSearch,
QueryConsumingAssetPatternSearch as QueryConsumingAssetPatternSearch,
QueryHasAssetScheduleFilter as QueryHasAssetScheduleFilter,
+ QueryHasEventsFilter as QueryHasEventsFilter,
QueryPartitionedDagRunDagIdFilter as QueryPartitionedDagRunDagIdFilter,
QueryPartitionedDagRunHasCreatedDagRunIdFilter as
QueryPartitionedDagRunHasCreatedDagRunIdFilter,
QueryUriExactMatch as QueryUriExactMatch,
diff --git a/airflow-core/src/airflow/api_fastapi/common/parameters/asset.py
b/airflow-core/src/airflow/api_fastapi/common/parameters/asset.py
index ba4c075ef21..5ace5e47828 100644
--- a/airflow-core/src/airflow/api_fastapi/common/parameters/asset.py
+++ b/airflow-core/src/airflow/api_fastapi/common/parameters/asset.py
@@ -110,6 +110,28 @@ class _HasAssetScheduleFilter(BaseParam[bool]):
return cls().set_value(has_asset_schedule)
+class _HasEventsFilter(BaseParam[bool]):
+ """Filter assets that have events."""
+
+ def to_orm(self, select: Select) -> Select:
+ if self.value is None and self.skip_none:
+ return select
+
+ event_subquery = sql_select(AssetEvent.asset_id).distinct()
+
+ if self.value:
+ return select.where(AssetModel.id.in_(event_subquery))
+
+ return select.where(AssetModel.id.notin_(event_subquery))
+
+ @classmethod
+ def depends(
+ cls,
+ has_events: bool | None = Query(None, description="Filter assets that
have events"),
+ ) -> _HasEventsFilter:
+ return cls().set_value(has_events)
+
+
class _AssetDependencyFilter(BaseParam[str]):
"""Filter Dags by specific asset dependencies."""
@@ -143,6 +165,7 @@ class _AssetDependencyFilter(BaseParam[str]):
QueryHasAssetScheduleFilter = Annotated[_HasAssetScheduleFilter,
Depends(_HasAssetScheduleFilter.depends)]
+QueryHasEventsFilter = Annotated[_HasEventsFilter,
Depends(_HasEventsFilter.depends)]
QueryAssetDependencyFilter = Annotated[_AssetDependencyFilter,
Depends(_AssetDependencyFilter.depends)]
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 f41aa95eeaf..e5e8a4db4a3 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
@@ -209,6 +209,16 @@ paths:
items:
type: string
title: Dag Ids
+ - name: has_events
+ in: query
+ required: false
+ schema:
+ anyOf:
+ - type: boolean
+ - type: 'null'
+ description: Filter assets that have events
+ title: Has Events
+ description: Filter assets that have events
- name: only_active
in: query
required: false
diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/assets.py
b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/assets.py
index a7d36896daa..74d1cd0312d 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/assets.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/assets.py
@@ -31,6 +31,7 @@ from airflow.api_fastapi.common.parameters import (
QueryAssetGroupPrefixPatternSearch,
QueryAssetNamePatternSearch,
QueryAssetNamePrefixPatternSearch,
+ QueryHasEventsFilter,
QueryLimit,
QueryOffset,
QueryUriExactMatch,
@@ -93,6 +94,7 @@ def get_assets(
group_pattern: QueryAssetGroupPatternSearch,
group_prefix_pattern: QueryAssetGroupPrefixPatternSearch,
dag_ids: QueryAssetDagIdPatternSearch,
+ has_events: QueryHasEventsFilter,
only_active: Annotated[OnlyActiveFilter,
Depends(OnlyActiveFilter.depends)],
last_asset_event_timestamp_range: Annotated[
RangeFilter,
@@ -128,6 +130,7 @@ def get_assets(
group_pattern,
group_prefix_pattern,
dag_ids,
+ has_events,
last_asset_event_timestamp_range,
readable_assets_filter,
],
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
index 5f604612c0a..60c05efbc48 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
@@ -87,10 +87,11 @@ export const UseAssetServiceGetDagAssetQueuedEventKeyFn =
({ assetId, before, da
export type AssetServiceGetAssetsUiDefaultResponse = Awaited<ReturnType<typeof
AssetService.getAssetsUi>>;
export type AssetServiceGetAssetsUiQueryResult<TData =
AssetServiceGetAssetsUiDefaultResponse, TError = unknown> =
UseQueryResult<TData, TError>;
export const useAssetServiceGetAssetsUiKey = "AssetServiceGetAssetsUi";
-export const UseAssetServiceGetAssetsUiKeyFn = ({ dagIds, groupPattern,
groupPrefixPattern, lastAssetEventTimestampGt, lastAssetEventTimestampGte,
lastAssetEventTimestampLt, lastAssetEventTimestampLte, limit, namePattern,
namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern,
uriPrefixPattern }: {
+export const UseAssetServiceGetAssetsUiKeyFn = ({ dagIds, groupPattern,
groupPrefixPattern, hasEvents, lastAssetEventTimestampGt,
lastAssetEventTimestampGte, lastAssetEventTimestampLt,
lastAssetEventTimestampLte, limit, namePattern, namePrefixPattern, offset,
onlyActive, orderBy, uri, uriPattern, uriPrefixPattern }: {
dagIds?: string[];
groupPattern?: string;
groupPrefixPattern?: string;
+ hasEvents?: boolean;
lastAssetEventTimestampGt?: string;
lastAssetEventTimestampGte?: string;
lastAssetEventTimestampLt?: string;
@@ -104,7 +105,7 @@ export const UseAssetServiceGetAssetsUiKeyFn = ({ dagIds,
groupPattern, groupPre
uri?: string[];
uriPattern?: string;
uriPrefixPattern?: string;
-} = {}, queryKey?: Array<unknown>) => [useAssetServiceGetAssetsUiKey,
...(queryKey ?? [{ dagIds, groupPattern, groupPrefixPattern,
lastAssetEventTimestampGt, lastAssetEventTimestampGte,
lastAssetEventTimestampLt, lastAssetEventTimestampLte, limit, namePattern,
namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern,
uriPrefixPattern }])];
+} = {}, queryKey?: Array<unknown>) => [useAssetServiceGetAssetsUiKey,
...(queryKey ?? [{ dagIds, groupPattern, groupPrefixPattern, hasEvents,
lastAssetEventTimestampGt, lastAssetEventTimestampGte,
lastAssetEventTimestampLt, lastAssetEventTimestampLte, limit, namePattern,
namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern,
uriPrefixPattern }])];
export type AssetServiceNextRunAssetsDefaultResponse =
Awaited<ReturnType<typeof AssetService.nextRunAssets>>;
export type AssetServiceNextRunAssetsQueryResult<TData =
AssetServiceNextRunAssetsDefaultResponse, TError = unknown> =
UseQueryResult<TData, TError>;
export const useAssetServiceNextRunAssetsKey = "AssetServiceNextRunAssets";
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 05bbc8be639..25b66d11174 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
@@ -172,6 +172,7 @@ export const
ensureUseAssetServiceGetDagAssetQueuedEventData = (queryClient: Que
* @param data.groupPattern Case-insensitive substring match (SQL `ILIKE`).
Slower than `group_prefix_pattern` on large tables — see "Filtering with
pattern parameters".
* @param data.groupPrefixPattern Case-sensitive, index-friendly prefix match.
See "Filtering with pattern parameters".
* @param data.dagIds
+* @param data.hasEvents Filter assets that have events
* @param data.onlyActive
* @param data.lastAssetEventTimestampGte
* @param data.lastAssetEventTimestampGt
@@ -181,10 +182,11 @@ export const
ensureUseAssetServiceGetDagAssetQueuedEventData = (queryClient: Que
* @returns AssetCollectionResponse Successful Response
* @throws ApiError
*/
-export const ensureUseAssetServiceGetAssetsUiData = (queryClient: QueryClient,
{ dagIds, groupPattern, groupPrefixPattern, lastAssetEventTimestampGt,
lastAssetEventTimestampGte, lastAssetEventTimestampLt,
lastAssetEventTimestampLte, limit, namePattern, namePrefixPattern, offset,
onlyActive, orderBy, uri, uriPattern, uriPrefixPattern }: {
+export const ensureUseAssetServiceGetAssetsUiData = (queryClient: QueryClient,
{ dagIds, groupPattern, groupPrefixPattern, hasEvents,
lastAssetEventTimestampGt, lastAssetEventTimestampGte,
lastAssetEventTimestampLt, lastAssetEventTimestampLte, limit, namePattern,
namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern,
uriPrefixPattern }: {
dagIds?: string[];
groupPattern?: string;
groupPrefixPattern?: string;
+ hasEvents?: boolean;
lastAssetEventTimestampGt?: string;
lastAssetEventTimestampGte?: string;
lastAssetEventTimestampLt?: string;
@@ -198,7 +200,7 @@ export const ensureUseAssetServiceGetAssetsUiData =
(queryClient: QueryClient, {
uri?: string[];
uriPattern?: string;
uriPrefixPattern?: string;
-} = {}) => queryClient.ensureQueryData({ queryKey:
Common.UseAssetServiceGetAssetsUiKeyFn({ dagIds, groupPattern,
groupPrefixPattern, lastAssetEventTimestampGt, lastAssetEventTimestampGte,
lastAssetEventTimestampLt, lastAssetEventTimestampLte, limit, namePattern,
namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern,
uriPrefixPattern }), queryFn: () => AssetService.getAssetsUi({ dagIds,
groupPattern, groupPrefixPattern, lastAssetEventTimestampGt,
lastAssetEventTimestampGte, las [...]
+} = {}) => queryClient.ensureQueryData({ queryKey:
Common.UseAssetServiceGetAssetsUiKeyFn({ dagIds, groupPattern,
groupPrefixPattern, hasEvents, lastAssetEventTimestampGt,
lastAssetEventTimestampGte, lastAssetEventTimestampLt,
lastAssetEventTimestampLte, limit, namePattern, namePrefixPattern, offset,
onlyActive, orderBy, uri, uriPattern, uriPrefixPattern }), queryFn: () =>
AssetService.getAssetsUi({ dagIds, groupPattern, groupPrefixPattern, hasEvents,
lastAssetEventTimestampGt, lastAsset [...]
/**
* Next Run Assets
* @param data The data for the request.
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 c1c988437f7..0f8b2db2660 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
@@ -172,6 +172,7 @@ export const prefetchUseAssetServiceGetDagAssetQueuedEvent
= (queryClient: Query
* @param data.groupPattern Case-insensitive substring match (SQL `ILIKE`).
Slower than `group_prefix_pattern` on large tables — see "Filtering with
pattern parameters".
* @param data.groupPrefixPattern Case-sensitive, index-friendly prefix match.
See "Filtering with pattern parameters".
* @param data.dagIds
+* @param data.hasEvents Filter assets that have events
* @param data.onlyActive
* @param data.lastAssetEventTimestampGte
* @param data.lastAssetEventTimestampGt
@@ -181,10 +182,11 @@ export const
prefetchUseAssetServiceGetDagAssetQueuedEvent = (queryClient: Query
* @returns AssetCollectionResponse Successful Response
* @throws ApiError
*/
-export const prefetchUseAssetServiceGetAssetsUi = (queryClient: QueryClient, {
dagIds, groupPattern, groupPrefixPattern, lastAssetEventTimestampGt,
lastAssetEventTimestampGte, lastAssetEventTimestampLt,
lastAssetEventTimestampLte, limit, namePattern, namePrefixPattern, offset,
onlyActive, orderBy, uri, uriPattern, uriPrefixPattern }: {
+export const prefetchUseAssetServiceGetAssetsUi = (queryClient: QueryClient, {
dagIds, groupPattern, groupPrefixPattern, hasEvents, lastAssetEventTimestampGt,
lastAssetEventTimestampGte, lastAssetEventTimestampLt,
lastAssetEventTimestampLte, limit, namePattern, namePrefixPattern, offset,
onlyActive, orderBy, uri, uriPattern, uriPrefixPattern }: {
dagIds?: string[];
groupPattern?: string;
groupPrefixPattern?: string;
+ hasEvents?: boolean;
lastAssetEventTimestampGt?: string;
lastAssetEventTimestampGte?: string;
lastAssetEventTimestampLt?: string;
@@ -198,7 +200,7 @@ export const prefetchUseAssetServiceGetAssetsUi =
(queryClient: QueryClient, { d
uri?: string[];
uriPattern?: string;
uriPrefixPattern?: string;
-} = {}) => queryClient.prefetchQuery({ queryKey:
Common.UseAssetServiceGetAssetsUiKeyFn({ dagIds, groupPattern,
groupPrefixPattern, lastAssetEventTimestampGt, lastAssetEventTimestampGte,
lastAssetEventTimestampLt, lastAssetEventTimestampLte, limit, namePattern,
namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern,
uriPrefixPattern }), queryFn: () => AssetService.getAssetsUi({ dagIds,
groupPattern, groupPrefixPattern, lastAssetEventTimestampGt,
lastAssetEventTimestampGte, lastA [...]
+} = {}) => queryClient.prefetchQuery({ queryKey:
Common.UseAssetServiceGetAssetsUiKeyFn({ dagIds, groupPattern,
groupPrefixPattern, hasEvents, lastAssetEventTimestampGt,
lastAssetEventTimestampGte, lastAssetEventTimestampLt,
lastAssetEventTimestampLte, limit, namePattern, namePrefixPattern, offset,
onlyActive, orderBy, uri, uriPattern, uriPrefixPattern }), queryFn: () =>
AssetService.getAssetsUi({ dagIds, groupPattern, groupPrefixPattern, hasEvents,
lastAssetEventTimestampGt, lastAssetEv [...]
/**
* Next Run Assets
* @param data The data for the request.
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 8c816d0aa72..d50d14d7c1b 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
@@ -172,6 +172,7 @@ export const useAssetServiceGetDagAssetQueuedEvent = <TData
= Common.AssetServic
* @param data.groupPattern Case-insensitive substring match (SQL `ILIKE`).
Slower than `group_prefix_pattern` on large tables — see "Filtering with
pattern parameters".
* @param data.groupPrefixPattern Case-sensitive, index-friendly prefix match.
See "Filtering with pattern parameters".
* @param data.dagIds
+* @param data.hasEvents Filter assets that have events
* @param data.onlyActive
* @param data.lastAssetEventTimestampGte
* @param data.lastAssetEventTimestampGt
@@ -181,10 +182,11 @@ export const useAssetServiceGetDagAssetQueuedEvent =
<TData = Common.AssetServic
* @returns AssetCollectionResponse Successful Response
* @throws ApiError
*/
-export const useAssetServiceGetAssetsUi = <TData =
Common.AssetServiceGetAssetsUiDefaultResponse, TError = unknown, TQueryKey
extends Array<unknown> = unknown[]>({ dagIds, groupPattern, groupPrefixPattern,
lastAssetEventTimestampGt, lastAssetEventTimestampGte,
lastAssetEventTimestampLt, lastAssetEventTimestampLte, limit, namePattern,
namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern,
uriPrefixPattern }: {
+export const useAssetServiceGetAssetsUi = <TData =
Common.AssetServiceGetAssetsUiDefaultResponse, TError = unknown, TQueryKey
extends Array<unknown> = unknown[]>({ dagIds, groupPattern, groupPrefixPattern,
hasEvents, lastAssetEventTimestampGt, lastAssetEventTimestampGte,
lastAssetEventTimestampLt, lastAssetEventTimestampLte, limit, namePattern,
namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern,
uriPrefixPattern }: {
dagIds?: string[];
groupPattern?: string;
groupPrefixPattern?: string;
+ hasEvents?: boolean;
lastAssetEventTimestampGt?: string;
lastAssetEventTimestampGte?: string;
lastAssetEventTimestampLt?: string;
@@ -198,7 +200,7 @@ export const useAssetServiceGetAssetsUi = <TData =
Common.AssetServiceGetAssetsU
uri?: string[];
uriPattern?: string;
uriPrefixPattern?: string;
-} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>,
"queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey:
Common.UseAssetServiceGetAssetsUiKeyFn({ dagIds, groupPattern,
groupPrefixPattern, lastAssetEventTimestampGt, lastAssetEventTimestampGte,
lastAssetEventTimestampLt, lastAssetEventTimestampLte, limit, namePattern,
namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern,
uriPrefixPattern }, queryKey), queryFn: () => AssetService.getAssetsUi({ [...]
+} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>,
"queryKey" | "queryFn">) => useQuery<TData, TError>({ queryKey:
Common.UseAssetServiceGetAssetsUiKeyFn({ dagIds, groupPattern,
groupPrefixPattern, hasEvents, lastAssetEventTimestampGt,
lastAssetEventTimestampGte, lastAssetEventTimestampLt,
lastAssetEventTimestampLte, limit, namePattern, namePrefixPattern, offset,
onlyActive, orderBy, uri, uriPattern, uriPrefixPattern }, queryKey), queryFn:
() => AssetService.get [...]
/**
* Next Run Assets
* @param data The data for the request.
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 646259af57b..b0979f8e663 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
@@ -172,6 +172,7 @@ export const useAssetServiceGetDagAssetQueuedEventSuspense
= <TData = Common.Ass
* @param data.groupPattern Case-insensitive substring match (SQL `ILIKE`).
Slower than `group_prefix_pattern` on large tables — see "Filtering with
pattern parameters".
* @param data.groupPrefixPattern Case-sensitive, index-friendly prefix match.
See "Filtering with pattern parameters".
* @param data.dagIds
+* @param data.hasEvents Filter assets that have events
* @param data.onlyActive
* @param data.lastAssetEventTimestampGte
* @param data.lastAssetEventTimestampGt
@@ -181,10 +182,11 @@ export const
useAssetServiceGetDagAssetQueuedEventSuspense = <TData = Common.Ass
* @returns AssetCollectionResponse Successful Response
* @throws ApiError
*/
-export const useAssetServiceGetAssetsUiSuspense = <TData =
Common.AssetServiceGetAssetsUiDefaultResponse, TError = unknown, TQueryKey
extends Array<unknown> = unknown[]>({ dagIds, groupPattern, groupPrefixPattern,
lastAssetEventTimestampGt, lastAssetEventTimestampGte,
lastAssetEventTimestampLt, lastAssetEventTimestampLte, limit, namePattern,
namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern,
uriPrefixPattern }: {
+export const useAssetServiceGetAssetsUiSuspense = <TData =
Common.AssetServiceGetAssetsUiDefaultResponse, TError = unknown, TQueryKey
extends Array<unknown> = unknown[]>({ dagIds, groupPattern, groupPrefixPattern,
hasEvents, lastAssetEventTimestampGt, lastAssetEventTimestampGte,
lastAssetEventTimestampLt, lastAssetEventTimestampLte, limit, namePattern,
namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern,
uriPrefixPattern }: {
dagIds?: string[];
groupPattern?: string;
groupPrefixPattern?: string;
+ hasEvents?: boolean;
lastAssetEventTimestampGt?: string;
lastAssetEventTimestampGte?: string;
lastAssetEventTimestampLt?: string;
@@ -198,7 +200,7 @@ export const useAssetServiceGetAssetsUiSuspense = <TData =
Common.AssetServiceGe
uri?: string[];
uriPattern?: string;
uriPrefixPattern?: string;
-} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>,
"queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey:
Common.UseAssetServiceGetAssetsUiKeyFn({ dagIds, groupPattern,
groupPrefixPattern, lastAssetEventTimestampGt, lastAssetEventTimestampGte,
lastAssetEventTimestampLt, lastAssetEventTimestampLte, limit, namePattern,
namePrefixPattern, offset, onlyActive, orderBy, uri, uriPattern,
uriPrefixPattern }, queryKey), queryFn: () => AssetService.getAss [...]
+} = {}, queryKey?: TQueryKey, options?: Omit<UseQueryOptions<TData, TError>,
"queryKey" | "queryFn">) => useSuspenseQuery<TData, TError>({ queryKey:
Common.UseAssetServiceGetAssetsUiKeyFn({ dagIds, groupPattern,
groupPrefixPattern, hasEvents, lastAssetEventTimestampGt,
lastAssetEventTimestampGte, lastAssetEventTimestampLt,
lastAssetEventTimestampLte, limit, namePattern, namePrefixPattern, offset,
onlyActive, orderBy, uri, uriPattern, uriPrefixPattern }, queryKey), queryFn:
() => AssetSer [...]
/**
* Next Run Assets
* @param data The data for the request.
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 38227629a2c..37f6155ee3c 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
@@ -421,6 +421,7 @@ export class AssetService {
* @param data.groupPattern Case-insensitive substring match (SQL
`ILIKE`). Slower than `group_prefix_pattern` on large tables — see "Filtering
with pattern parameters".
* @param data.groupPrefixPattern Case-sensitive, index-friendly prefix
match. See "Filtering with pattern parameters".
* @param data.dagIds
+ * @param data.hasEvents Filter assets that have events
* @param data.onlyActive
* @param data.lastAssetEventTimestampGte
* @param data.lastAssetEventTimestampGt
@@ -445,6 +446,7 @@ export class AssetService {
group_pattern: data.groupPattern,
group_prefix_pattern: data.groupPrefixPattern,
dag_ids: data.dagIds,
+ has_events: data.hasEvents,
only_active: data.onlyActive,
last_asset_event_timestamp_gte:
data.lastAssetEventTimestampGte,
last_asset_event_timestamp_gt: data.lastAssetEventTimestampGt,
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 f279f7d2cea..b1250bdbc04 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
@@ -3276,6 +3276,10 @@ export type GetAssetsUiData = {
* Case-sensitive, index-friendly prefix match. See "Filtering with
pattern parameters".
*/
groupPrefixPattern?: string | null;
+ /**
+ * Filter assets that have events
+ */
+ hasEvents?: boolean | null;
lastAssetEventTimestampGt?: string | null;
lastAssetEventTimestampGte?: string | null;
lastAssetEventTimestampLt?: string | null;
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/assets.json
b/airflow-core/src/airflow/ui/public/i18n/locales/en/assets.json
index 5445bc71c49..bc0bb75f282 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/en/assets.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/assets.json
@@ -49,6 +49,7 @@
"events": "Events",
"filters": {
"groupPlaceholder": "Search group",
+ "hasEvents": "Has Events",
"lastEventDateRange": "Last Event Date"
},
"group": "Group",
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 445387fcc89..8caa9019a45 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
@@ -199,6 +199,7 @@
"plugins": "Plugins",
"security": "Security"
},
+ "no": "No",
"noItemsFound": "No {{modelName}} found",
"note": {
"add": "Add a note",
@@ -584,5 +585,6 @@
"tooltip": "Press {{hotkey}} to toggle wrap",
"unwrap": "Unwrap",
"wrap": "Wrap"
- }
+ },
+ "yes": "Yes"
}
diff --git a/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx
b/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx
index 0bed10d2958..4e5a7afa6a7 100644
--- a/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx
+++ b/airflow-core/src/airflow/ui/src/constants/filterConfigs.tsx
@@ -249,6 +249,15 @@ export const useFilterConfigs = () => {
supportsAdvancedSearch: true,
type: FilterTypes.TEXT,
},
+ [SearchParamsKeys.HAS_EVENTS]: {
+ icon: <MdCheckCircle />,
+ label: translate("assets:filters.hasEvents"),
+ options: [
+ { label: translate("common:yes"), value: "true" },
+ { label: translate("common:no"), value: "false" },
+ ],
+ type: FilterTypes.SELECT,
+ },
[SearchParamsKeys.HOSTNAME]: {
hotkeyDisabled: true,
icon: <MdComputer />,
diff --git a/airflow-core/src/airflow/ui/src/constants/searchParams.ts
b/airflow-core/src/airflow/ui/src/constants/searchParams.ts
index 9edc3ae60b9..195a8eccc1c 100644
--- a/airflow-core/src/airflow/ui/src/constants/searchParams.ts
+++ b/airflow-core/src/airflow/ui/src/constants/searchParams.ts
@@ -62,6 +62,7 @@ export enum SearchParamsKeys {
GRAPH_TASK_GROUP = "graph-task_group",
GRAPH_TASK_STATE = "graph-task_state",
GROUP_PATTERN = "group_pattern",
+ HAS_EVENTS = "has_events",
HOSTNAME = "hostname",
INCLUDED_EVENTS = "included_events",
JOB_STATE = "job_state",
diff --git
a/airflow-core/src/airflow/ui/src/pages/AssetsList/AssetsList.test.tsx
b/airflow-core/src/airflow/ui/src/pages/AssetsList/AssetsList.test.tsx
index 0e15c6671fc..ab00103a478 100644
--- a/airflow-core/src/airflow/ui/src/pages/AssetsList/AssetsList.test.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/AssetsList/AssetsList.test.tsx
@@ -56,6 +56,34 @@ describe("AssetsList columns", () => {
});
describe("AssetsList filtering", () => {
+ it.each([
+ { expectedLabel: "yes", hasEvents: "true" },
+ { expectedLabel: "no", hasEvents: "false" },
+ { expectedLabel: undefined, hasEvents: null },
+ ])("restores has_events=$hasEvents from the URL", async ({ expectedLabel,
hasEvents }) => {
+ let requestedHasEvents: string | null | undefined;
+
+ server.use(
+ http.get("/ui/assets", ({ request }) => {
+ requestedHasEvents = new
URL(request.url).searchParams.get("has_events");
+
+ return HttpResponse.json({ assets: [], total_entries: 0 });
+ }),
+ );
+
+ const initialUrl = hasEvents === null ? "/assets" :
`/assets?has_events=${hasEvents}`;
+
+ render(<AppWrapper initialEntries={[initialUrl]} />);
+
+ await waitFor(() => expect(requestedHasEvents).toBe(hasEvents));
+
+ if (expectedLabel === undefined) {
+ expect(screen.queryByTestId("has_events-pill")).not.toBeInTheDocument();
+ } else {
+
expect(screen.getByTestId("has_events-pill")).toHaveTextContent(`filters.hasEvents:
${expectedLabel}`);
+ }
+ });
+
it("keeps the listed assets on screen while a filter change is still
loading", async () => {
render(<AppWrapper initialEntries={["/assets"]} />);
diff --git a/airflow-core/src/airflow/ui/src/pages/AssetsList/AssetsList.tsx
b/airflow-core/src/airflow/ui/src/pages/AssetsList/AssetsList.tsx
index 0b4a3c0db01..ec81acb41f6 100644
--- a/airflow-core/src/airflow/ui/src/pages/AssetsList/AssetsList.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/AssetsList/AssetsList.tsx
@@ -45,6 +45,7 @@ import { DependencyPopover } from "./DependencyPopover";
const assetsFilterKeys: Array<FilterableSearchParamsKeys> = [
SearchParamsKeys.GROUP_PATTERN,
+ SearchParamsKeys.HAS_EVENTS,
SearchParamsKeys.LAST_ASSET_EVENT_TIMESTAMP_RANGE,
];
@@ -147,6 +148,15 @@ export const AssetsList = () => {
const { filterConfigs, handleFiltersChange, initialValues } =
useFiltersHandler(assetsFilterKeys);
+ const hasEventsParam = searchParams.get(SearchParamsKeys.HAS_EVENTS);
+ let hasEvents = undefined;
+
+ if (hasEventsParam === "true") {
+ hasEvents = true;
+ } else if (hasEventsParam === "false") {
+ hasEvents = false;
+ }
+
const lastAssetEventTimestampGte =
searchParams.get(SearchParamsKeys.LAST_ASSET_EVENT_TIMESTAMP_GTE);
const lastAssetEventTimestampLte =
searchParams.get(SearchParamsKeys.LAST_ASSET_EVENT_TIMESTAMP_LTE);
const groupArg = useAdvancedSearchArg({
@@ -159,6 +169,7 @@ export const AssetsList = () => {
const { data, error, isFetching, isLoading } = useAssetServiceGetAssetsUi(
{
...groupArg,
+ hasEvents,
lastAssetEventTimestampGte: lastAssetEventTimestampGte ?? undefined,
lastAssetEventTimestampLte: lastAssetEventTimestampLte ?? undefined,
limit: pagination.pageSize,
diff --git a/airflow-core/src/airflow/ui/src/utils/useFiltersHandler.ts
b/airflow-core/src/airflow/ui/src/utils/useFiltersHandler.ts
index 61c6db81769..4b70b9a7ab2 100644
--- a/airflow-core/src/airflow/ui/src/utils/useFiltersHandler.ts
+++ b/airflow-core/src/airflow/ui/src/utils/useFiltersHandler.ts
@@ -78,6 +78,7 @@ export type FilterableSearchParamsKeys =
| SearchParamsKeys.FAVORITE
| SearchParamsKeys.FROM_RANGE
| SearchParamsKeys.GROUP_PATTERN
+ | SearchParamsKeys.HAS_EVENTS
| SearchParamsKeys.HOSTNAME
| SearchParamsKeys.JOB_STATE
| SearchParamsKeys.JOB_TYPE
diff --git
a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_assets.py
b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_assets.py
index e0a87013c4d..7cc99a96ff1 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_assets.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_assets.py
@@ -738,6 +738,29 @@ class TestGetAssetsUi:
assert response.status_code == 200
assert [a["name"] for a in response.json()["assets"]] == ["newer"]
+ @pytest.mark.parametrize(
+ ("has_events", "expected_names"),
+ [
+ pytest.param(None, ["evented", "never"], id="unset"),
+ pytest.param(True, ["evented"], id="has-events"),
+ pytest.param(False, ["never"], id="no-events"),
+ ],
+ )
+ def test_filter_by_has_events(self, test_client, session, has_events,
expected_names):
+ evented = AssetModel(name="evented", uri="s3://bucket/evented",
group="asset")
+ never = AssetModel(name="never", uri="s3://bucket/never",
group="asset")
+ session.add_all([evented, never])
+ session.add_all([AssetActive.for_asset(evented),
AssetActive.for_asset(never)])
+ session.flush()
+ session.add(AssetEvent(asset_id=evented.id,
timestamp=pendulum.datetime(2024, 1, 1)))
+ session.commit()
+
+ params = {} if has_events is None else {"has_events": has_events}
+ response = test_client.get("/assets", params=params)
+
+ assert response.status_code == 200
+ assert sorted(asset["name"] for asset in response.json()["assets"]) ==
expected_names
+
def test_aliases_present_for_asset_via_alias(self, test_client, session):
"""
Regression test for https://github.com/apache/airflow/issues/58058: